Add twitter forwarder frontend (#2038)

* Got basic add twitter forwarder account working

* Got delete twitter account working

* Link to profile page from twitter account card

* Simplify text for delete twitter account button

* Update frontend build
This commit is contained in:
Jonathan Zernik 2022-03-28 00:06:09 -07:00 committed by GitHub
parent b578ada984
commit c3930b8fe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 872 additions and 98 deletions

View file

@ -12,6 +12,7 @@ import Payments from './components/Payments'
import Peers from './components/Peers'
import Feed from './components/Feed'
import Search from './components/Search'
import Twitter from './components/Twitter'
import Notifications from './components/Notifications'
import Alerts from './components/Alerts'
@ -50,6 +51,9 @@ const DefaultContainer = withRouter(({ history }) => {
<Route path="/app/peers" exact>
<Peers/>
</Route>
<Route path="/app/twitter" exact>
<Twitter/>
</Route>
<Route path="/app/notifications" exact>
<Notifications/>
</Route>

View file

@ -744,3 +744,40 @@ export const downloadSqueak = (squeakHash) => {
deser: deser,
});
}
export const getTwitterAccounts = () => {
console.log('Calling getTwitterAccounts');
const request = new GetTwitterAccountsRequest();
const deser = GetTwitterAccountsReply.deserializeBinary;
return baseRequest({
url: '/gettwitteraccounts',
req: request,
deser: deser,
});
}
export const createTwitterAccount = (twitterHandle, profileId, bearerToken) => {
console.log('Calling createTwitterAccount');
const request = new AddTwitterAccountRequest();
request.setHandle(twitterHandle);
request.setProfileId(profileId);
request.setBearerToken(bearerToken);
const deser = AddTwitterAccountReply.deserializeBinary;
return baseRequest({
url: '/addtwitteraccount',
req: request,
deser: deser,
});
}
export const deleteTwitterAccount = (twitterAccountId) => {
console.log('Calling deleteTwitterAccount');
const request = new DeleteTwitterAccountRequest();
request.setTwitterAccountId(twitterAccountId);
const deser = DeleteTwitterAccountReply.deserializeBinary;
return baseRequest({
url: '/deletetwitteraccount',
req: request,
deser: deser,
});
}

View file

@ -0,0 +1,175 @@
import React, { useEffect, useState, useContext } from 'react'
import './style.scss'
import { withRouter, Link } from 'react-router-dom'
import { ICON_SEARCH, ICON_ARROWBACK, ICON_CLOSE } from '../../Icons'
import { getProfileImageSrcString } from '../../squeakimages/images';
import Loader from '../Loader'
import SqueakCard from '../SqueakCard'
import Select from 'react-select'
import { unwrapResult } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import { useSelector } from 'react-redux'
import TwitterAccounts from '../../features/twitterAccounts/TwitterAccounts'
import {
setCreateSigningProfile,
} from '../../features/profiles/profilesSlice'
import {
setCreateTwitterAccount,
} from '../../features/twitterAccounts/twitterAccountsSlice'
import {
selectSigningProfiles,
fetchSigningProfiles,
} from '../../features/profiles/profilesSlice'
const Twitter = (props) => {
const [tab, setTab] = useState('Signing Profiles')
const [twitterAccountModalOpen, setTwitterAccountModalOpen] = useState(false)
const [styleBody, setStyleBody] = useState(false)
const [twitterHandle, setTwitterHandle] = useState('')
const [newProfilePubkey, setNewProfilePubkey] = useState('')
const [bearerToken, setBearerToken] = useState('')
const [signingProfile, setSigningProfile] = useState(null)
const signingProfiles = useSelector(selectSigningProfiles);
const dispatch = useDispatch()
const searchOnChange = (param) => {
if(tab !== 'Search'){setTab('Search')}
if(param.length>0){
// TODO: search for a profile by name.
}
}
const optionsFromProfiles = (profiles) => {
return profiles.map((p) => {
return { value: p, label: p.getProfileName() }
// return { value: 'chocolate', label: 'Chocolate' }
});
}
const toggleCreateTwitterAccountModal = (param, type) => {
setStyleBody(!styleBody)
setTimeout(()=>{ setTwitterAccountModalOpen(!twitterAccountModalOpen) },20)
}
const createTwitterAccount = () => {
console.log('Create twitter account with handle:', twitterHandle);
if (!signingProfile) {
alert('Signing profile must be set.');
return;
}
dispatch(setCreateTwitterAccount({
twitterHandle: twitterHandle,
profileId: signingProfile.getProfileId(),
bearerToken: bearerToken,
}))
.then(unwrapResult)
.then((pubkey) => {
console.log('Created twitter account with handle', twitterHandle);
})
.catch((err) => {
alert(err.message);
});
toggleCreateTwitterAccountModal();
}
const handleModalClick = (e) => {
e.stopPropagation()
}
const handleChangeSigningProfile = (e) => {
setSigningProfile(e.value);
}
return(
<div>
<div className="explore-wrapper">
<div className="explore-header">
<div className="explore-search-wrapper">
<div className="explore-search-icon">
<ICON_SEARCH/>
</div>
<div className="explore-search-input">
<input onChange={(e)=>searchOnChange(e.target.value)} placeholder="Search for people" type="text" name="search"/>
</div>
</div>
</div>
<div className="profile-details-wrapper">
<div className="profiles-options">
<div onClick={(e)=>toggleCreateTwitterAccountModal('edit')}
className='profiles-create-button'>
<span>Add Twitter Account</span>
</div>
</div>
</div>
<div>
<div className="explore-nav-menu">
<div onClick={()=>setTab('Signing Profiles')} className={tab === 'Signing Profiles' ? `explore-nav-item activeTab` : `explore-nav-item`}>
Twitter Accounts
</div>
</div>
<TwitterAccounts />
</div>
</div>
{/* Modal for create signing profile */}
<div onClick={()=>toggleCreateTwitterAccountModal()} style={{display: twitterAccountModalOpen ? 'block' : 'none'}} className="modal-edit">
<div onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleCreateTwitterAccountModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Add Twitter Account</p>
<div className="save-modal-wrapper">
<div onClick={createTwitterAccount} className="save-modal-btn">
Submit
</div>
</div>
</div>
<div className="modal-body">
<form className="edit-form">
<div className="edit-input-wrap">
<div className="edit-input-content">
<label>Twitter Handle</label>
<input onChange={(e)=>setTwitterHandle(e.target.value)} type="text" name="name" className="edit-input"/>
</div>
</div>
<div className="edit-input-wrap">
<div className="edit-input-content">
<label>Twitter Bearer Token</label>
<input onChange={(e)=>setBearerToken(e.target.value)} type="text" name="name" className="edit-input"/>
</div>
</div>
<div className="edit-input-wrap">
<div className="edit-input-content">
<label>Signing Profile</label>
<div className="inner-input-box">
<Select options={optionsFromProfiles(signingProfiles)} onChange={handleChangeSigningProfile} />
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
)
}
export default withRouter(Twitter)

View file

@ -0,0 +1,298 @@
.explore-wrapper{
max-width: 600px;
border-right: 1px solid rgb(230, 236, 240);
width: 100%;
// height: 100vh;
display: flex;
flex-direction: column;
min-height: 2000px;
}
.explore-header{
position: sticky;
border-left: 1px solid rgb(230, 236, 240);
background-color: #fff;
z-index: 8;
top: 0px;
display: flex;
align-items: center;
height: 53px;
min-height: 53px;
padding-left: 15px;
padding-right: 15px;
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.header-border{
border-bottom: 1px solid rgb(230, 236, 240);
}
.explore-search-wrapper{
background-color: rgb(230, 236, 240);
border: 1px solid transparent;
border-radius: 9999px;
display: flex;
align-items: center;
min-height: 38px;
width: 100%;
}
.explore-search-icon{
display: flex;
align-items: center;
}
.explore-search-icon svg{
width: 40px;
height: 18.75px;
fill: rgb(101, 119, 134);
padding-left: 10px;
}
.explore-search-input{
width: 100%;
}
.explore-search-input input{
background-color: inherit;
border: inherit;
padding: 6px 10px;
width: 100%;
font-size: 15px;
color: rgb(101, 119, 134);
outline: none;
}
.explore-nav-menu{
margin-top: 10px;
display: flex;
justify-content: space-around;
align-items: center;
border-bottom: 1px solid rgb(230, 236, 240);
}
.explore-nav-item{
padding: 15px;
width: 100%;
text-align: center;
cursor: pointer;
font-weight: bold;
color: rgb(101, 119, 134);
transition: 0.2s;
will-change: background-color;
box-sizing:border-box;
border-bottom: 2px solid transparent;
&:hover{
background-color: rgba(29, 161, 242, 0.1);
color: rgb(29, 161, 242);
}
}
.activeTab{
border-bottom: 2px solid rgb(29, 161, 242);
color: rgb(29, 161, 242);
}
.search-result-wapper{
border-bottom: 1px solid rgb(230, 236, 240);
padding: 10px 15px;
transition: 0.2s;
cursor: pointer;
display: flex;
&:hover{
background-color: rgb(245,248,250);
}
}
.search-userPic-wrapper{
flex-basis: 49px;
margin-right: 10px;
img{
object-fit: cover;
}
}
.search-user-details{
display: flex;
flex-direction: column;
width: 100%;
}
.search-user-info{
display: flex;
flex-direction: column;
}
.search-user-name{
font-weight: bold;
}
.search-user-username{
color: rgb(101, 119, 134);
line-height: 1;
}
.search-user-bio{
margin-top: 7px;
}
.search-user-warp{
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.follow-btn-wrap{
min-height: 30px;
min-width: 70px;
transition: 0.2s ease-in-out;
cursor: pointer;
border: 1px solid #1da1f2;
border-radius: 9999px;
display: flex;
justify-content: center;
align-items: center;
padding-left: 1em;
padding-right: 1em;
&:hover{
background-color: rgba(29, 161, 242, 0.1);
}
}
.follow-btn-wrap span{
text-align: center;
font-weight: 800;
color: #1da1f2;
width: 100%;
}
.trending-card-wrapper{
border-bottom: 1px solid rgb(245,248,250);
padding: 10px 15px;
transition: 0.2s;
cursor: pointer;
display: flex;
flex-direction: column;
&:hover{
background-color: rgb(245, 248, 250);
}
}
.trending-card-header{
color: rgb(101, 119, 134);
font-size: 14px;
span{
padding: 0 3px;
}
}
.trending-card-content{
font-weight: bold;
font-size: 19px;
padding-top: 2px;
padding-bottom: 2px;
}
.trending-card-count{
font-size: 15px;
color: rgb(101, 119, 134);
}
.try-searching{
font-weight: bold;
font-size: 17px;
text-align: center;
margin-top: 40px;
color: #657786;
div{
margin-bottom: 15px;
}
}
.unfollow-switch{
background-color: rgb(29, 161, 242);
span{color: #fff !important;}
}
.unfollow-switch:hover{
background-color: rgb(202,32,85) !important;
border: 1px solid transparent;
span{
color: #fff;
span{display: none;}
&:before{
content: 'Unfollow';
}
}
}
.explore-header-back{
min-width: 55px;
min-height: 30px;
justify-content: center;
align-items: flex-start;
}
.explore-back-wrapper{
margin-left: -5px;
width: 39px;
height: 39px;
transition: 0.2s ease-in-out;
will-change: background-color;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 9999px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.explore-back-wrapper svg{
height: 1.5em;
fill: rgb(29,161,242);
}
.explore-back-wrapper:hover{
background-color: rgba(29,161,242,0.1);
}
.profiles-details-wrapper{
padding: 10px 15px 15px 15px;
}
.profiles-options{
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
}
.profiles-create-button{
min-height: 39px;
min-width: 98.8px;
transition: 0.2s ease-in-out;
cursor: pointer;
border: 1px solid rgb(29, 161, 242);
border-radius: 9999px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 7px;
padding-left: 1em;
padding-right: 1em;
span{
text-align: center;
font-weight: 800;
color: rgb(29, 161, 242);
width: 100%
}
&:hover{
background-color: rgba(29, 161, 242,0.1);
}
}

View file

@ -0,0 +1,98 @@
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'
import { withRouter, Link } from 'react-router-dom'
import moment from 'moment'
import { ICON_SETTINGS } from '../../Icons'
import { getProfileImageSrcString } from '../../squeakimages/images';
import SqueakCard from '../../components/SqueakCard'
import Loader from '../../components/Loader'
import {
setDeleteTwitterAccount,
} from './twitterAccountsSlice'
const TwitterAccountCard = (props) => {
const [moreMenu, setMoreMenu] = useState(false)
const [styleBody, setStyleBody] = useState(false)
const [saved, setSaved] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const twitterAccount = props.twitterAccount;
const profile = twitterAccount.getProfile();
const dispatch = useDispatch();
// const followUser = (e, id) => {
// e.stopPropagation()
// console.log('Follow clicked');
// dispatch(setFollowProfile(id));
// }
//
// const unfollowUser = (e,id) => {
// e.stopPropagation()
// console.log('Unfollow clicked');
// dispatch(setUnfollowProfile(id));
// }
const deleteTwitterAccount = (e) => {
e.stopPropagation()
console.log('Delete clicked');
dispatch(setDeleteTwitterAccount({
twitterAccountId: twitterAccount.getTwitterAccountId(),
}));
}
const openMore = () => { setMoreMenu(!moreMenu) }
const handleMenuClick = (e) => { e.stopPropagation() }
// const toggleDeleteModal = () => {
// setStyleBody(!styleBody)
// setSaved(false)
// setTimeout(()=>{ setDeleteModalOpen(!deleteModalOpen) },20)
// }
return <div onClick={(e)=>e.stopPropagation()} key={profile.getPubkey()} className="search-result-wapper">
<Link to={`/app/profile/${profile.getPubkey()}`} className="search-userPic-wrapper">
<img style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={`${getProfileImageSrcString(profile)}`}/>
</Link>
<div className="search-user-details">
<div className="search-user-warp">
<div className="search-user-info">
<div className="search-user-name">{profile.getProfileName()}</div>
<div className="search-user-username">@{profile.getPubkey()}</div>
</div>
</div>
<div className="search-user-username"><b>Twitter Handle</b>: <a href={`https://twitter.com/${twitterAccount.getHandle()}`} style={{color: "blue", fontWeight: 'bold'}}>
{twitterAccount.getHandle()}
</a></div>
<div className="search-user-bio">
&nbsp;
</div>
</div>
<div id="profileMoreMenu" onClick={openMore} className="Nav-link">
<div className={"Nav-item-hover"}>
<ICON_SETTINGS />
</div>
<div onClick={()=>openMore()} style={{display: moreMenu ? 'block' : 'none'}} className="more-menu-background">
<div className="more-modal-wrapper">
{moreMenu ?
<div style={{
top: document.getElementById('profileMoreMenu') && `${document.getElementById('profileMoreMenu').getBoundingClientRect().top - 40}px`,
left: document.getElementById('profileMoreMenu') && `${document.getElementById('profileMoreMenu').getBoundingClientRect().left}px`,
height: '40px',
}} onClick={(e)=>handleMenuClick(e)} className="more-menu-content">
<div onClick={deleteTwitterAccount} className="more-menu-item">
<span>Delete</span>
</div>
</div> : null }
</div>
</div>
</div>
</div>
}
export default withRouter(TwitterAccountCard)

View file

@ -0,0 +1,40 @@
import React, { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'
import { withRouter, Link } from 'react-router-dom'
import moment from 'moment'
import TwitterAccountCard from './TwitterAccountCard'
import Loader from '../../components/Loader'
import {
fetchTwitterAccounts,
clearTwitterAccounts,
selectTwitterAccounts,
selectTwitterAccountsStatus,
} from './twitterAccountsSlice'
const TwitterAccounts = (props) => {
const twitterAccounts = useSelector(selectTwitterAccounts);
const twitterAccountsStatus = useSelector(selectTwitterAccountsStatus);
const dispatch = useDispatch();
useEffect(() => {
window.scrollTo(0, 0)
console.log('fetchTwitterAccounts');
dispatch(clearTwitterAccounts());
dispatch(fetchTwitterAccounts(null));
}, [])
const renderedListItems = twitterAccounts.map(twitterAccount=>{
return <TwitterAccountCard twitterAccount={twitterAccount}/>
})
return <>
{renderedListItems}
</>
}
export default withRouter(TwitterAccounts)

View file

@ -0,0 +1,120 @@
import {
createSlice,
createSelector,
createAsyncThunk,
createEntityAdapter,
} from '@reduxjs/toolkit'
import {
getTwitterAccounts,
createTwitterAccount,
deleteTwitterAccount,
} from '../../api/client'
const initialState = {
twitterAccountsStatus: 'idle',
twitterAccounts: [],
createTwitterAccountStatus: 'idle',
}
export const setDeleteTwitterAccount = createAsyncThunk(
'twitterAccounts/setDeleteTwitterAccount',
async (values) => {
console.log('Deleting twitter account');
let twitterAccountId = values.twitterAccountId;
await deleteTwitterAccount(twitterAccountId);
const response = await getTwitterAccounts();
console.log(response);
return response.getTwitterAccountsList();
}
)
export const fetchTwitterAccounts = createAsyncThunk(
'twitterAccounts/fetchTwitterAccounts',
async () => {
const response = await getTwitterAccounts();
console.log(response);
return response.getTwitterAccountsList();
}
)
export const setCreateTwitterAccount = createAsyncThunk(
'twitterAccounts/setCreateTwitterAccount',
async (values) => {
console.log('Creating twitter account');
let twitterHandle = values.twitterHandle;
let profileId = values.profileId;
let bearerToken = values.bearerToken;
const createResponse = await createTwitterAccount(
twitterHandle,
profileId,
bearerToken,
);
console.log(createResponse);
const response = await getTwitterAccounts();
console.log(response);
return response.getTwitterAccountsList();
}
)
// const updatedProfileInArray = (profileArr, newProfile) => {
// const currentIndex = profileArr.findIndex(profile => profile.getPubkey() === newProfile.getPubkey());
// if (currentIndex != -1) {
// profileArr[currentIndex] = newProfile;
// }
// }
const twitterAccountsSlice = createSlice({
name: 'twitterAccounts',
initialState,
reducers: {
clearTwitterAccounts(state, action) {
state.createTwitterAccountStatus = 'idle'
state.twitterAccounts = [];
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTwitterAccounts.pending, (state, action) => {
state.twitterAccountsStatus = 'loading'
})
.addCase(fetchTwitterAccounts.fulfilled, (state, action) => {
const newTwitterAccounts = action.payload;
state.twitterAccounts = newTwitterAccounts;
state.twitterAccountsStatus = 'idle'
})
.addCase(setCreateTwitterAccount.pending, (state, action) => {
console.log('setCreateTwitterAccount pending');
state.createTwitterAccountStatus = 'loading'
})
.addCase(setCreateTwitterAccount.fulfilled, (state, action) => {
console.log('setCreateTwitterAccount fulfilled');
console.log(action);
const newTwitterAccounts = action.payload;
state.twitterAccounts = newTwitterAccounts;
state.twitterAccountsStatus = 'idle'
state.createTwitterAccountStatus = 'idle';
})
.addCase(setDeleteTwitterAccount.fulfilled, (state, action) => {
console.log(action);
const newTwitterAccounts = action.payload;
state.twitterAccounts = newTwitterAccounts;
state.twitterAccountsStatus = 'idle'
})
},
})
export const {
clearTwitterAccounts,
} = twitterAccountsSlice.actions
export default twitterAccountsSlice.reducer
export const selectTwitterAccountsStatus = state => state.twitterAccounts.twitterAccountsStatus
export const selectTwitterAccounts = state => state.twitterAccounts.twitterAccounts
export const selectCreateTwitterAccountStatus = state => state.twitterAccounts.createTwitterAccountStatus

View file

@ -8,6 +8,7 @@ import externalAddressReducer from './features/externalAddress/externalAddressSl
import paymentsReducer from './features/payments/paymentsSlice'
import sellPriceReducer from './features/sellPrice/sellPriceSlice'
import accountReducer from './features/account/accountSlice'
import twitterAccounsReducer from './features/twitterAccounts/twitterAccountsSlice'
const store = configureStore({
reducer: {
@ -19,6 +20,7 @@ const store = configureStore({
payments: paymentsReducer,
sellPrice: sellPriceReducer,
account: accountReducer,
twitterAccounts: twitterAccounsReducer,
},
})