From e147a5889f9696e747119adfa0f55764c39c560b Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Sun, 19 Nov 2023 21:47:42 +0000 Subject: [PATCH] Referrers, referrals --- app/ResolveRoute.js | 7 +- app/RootRoute.js | 2 + app/components/all.scss | 1 + app/components/elements/TimeAgoWrapper.js | 30 +++- app/components/modules/Header.jsx | 8 + app/components/modules/Referrals.jsx | 192 ++++++++++++++++++++++ app/components/modules/Referrals.scss | 10 ++ app/components/modules/TopRightMenu.jsx | 4 +- app/components/pages/Referrers.jsx | 158 ++++++++++++++++++ app/components/pages/UserProfile.jsx | 9 + app/locales/en.json | 30 ++++ app/locales/ru-RU.json | 30 ++++ app/redux/FetchDataSaga.js | 69 ++++++++ app/redux/GlobalReducer.js | 64 ++++++++ package.json | 3 +- yarn.lock | 103 +++++++++--- 16 files changed, 680 insertions(+), 40 deletions(-) create mode 100644 app/components/modules/Referrals.jsx create mode 100644 app/components/modules/Referrals.scss create mode 100644 app/components/pages/Referrers.jsx diff --git a/app/ResolveRoute.js b/app/ResolveRoute.js index d690921b..d55071d8 100644 --- a/app/ResolveRoute.js +++ b/app/ResolveRoute.js @@ -1,9 +1,9 @@ export const routeRegex = { PostsIndex: /^\/(@[\w\.\d-]+)\/feed\/?$/, UserProfile1: /^\/(@[\w\.\d-]+)\/?$/, - UserProfile2: /^\/(@[\w\.\d-]+)\/(blog|posts|comments|reputation|mentions|created|recent-replies|discussions|feed|followed|followers|sponsors|settings)\/??(?:&?[^=&]*=[^=&]*)*$/, + UserProfile2: /^\/(@[\w\.\d-]+)\/(blog|posts|comments|reputation|mentions|created|recent-replies|discussions|feed|followed|followers|sponsors|referrals|settings)\/??(?:&?[^=&]*=[^=&]*)*$/, UserProfile3: /^\/(@[\w\.\d-]+)\/[\w\.\d-]+/, - UserEndPoints: /^(blog|posts|comments|reputation|mentions|created|recent-replies|discussions|feed|followed|followers|sponsors|settings)$/, + UserEndPoints: /^(blog|posts|comments|reputation|mentions|created|recent-replies|discussions|feed|followed|followers|sponsors|referrals|settings)$/, CategoryFilters: /^\/(hot|responses|donates|forums|trending|promoted|allposts|allcomments|created|active)\/?$/ig, PostNoCategory: /^\/(@[\w\.\d-]+)\/([\w\d-]+)/, Post: /^\/([\w\d\-\/]+)\/(\@[\w\d\.-]+)\/([\w\d-]+)\/?($|\?)/, @@ -44,6 +44,9 @@ export default function resolveRoute(path) if (path === '/minused_accounts') { return {page: 'MinusedAccounts'}; } + if (path === '/referrers') { + return {page: 'Referrers'}; + } if (process.env.IS_APP) { if (path === '/__app_goto_url') { return {page: 'AppGotoURL'}; diff --git a/app/RootRoute.js b/app/RootRoute.js index 649843e8..7ab82e45 100644 --- a/app/RootRoute.js +++ b/app/RootRoute.js @@ -40,6 +40,8 @@ export default { cb(null, [require('@pages/TagsIndex')]); } else if (route.page === 'MinusedAccounts') { cb(null, [require('@pages/MinusedAccounts')]); + } else if (route.page === 'Referrers') { + cb(null, [require('@pages/Referrers')]) } else if (route.page === 'AppGotoURL') { cb(null, [require('@pages/app/AppGotoURL')]); } else if (route.page === 'AppSplash') { diff --git a/app/components/all.scss b/app/components/all.scss index aa90959f..b6ba1da3 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -72,6 +72,7 @@ @import "./modules/GiftNFT"; @import "./modules/Header"; @import "./modules/LoginForm"; +@import "./modules/Referrals"; @import "./modules/Settings"; @import "./modules/SidePanel"; @import "./modules/SignUp"; diff --git a/app/components/elements/TimeAgoWrapper.js b/app/components/elements/TimeAgoWrapper.js index 62d8aeb8..85e11154 100644 --- a/app/components/elements/TimeAgoWrapper.js +++ b/app/components/elements/TimeAgoWrapper.js @@ -1,20 +1,32 @@ /* eslint react/prop-types: 0 */ import React from 'react'; -import { FormattedRelativeTime } from 'react-intl' +import { FormattedRelativeTime, formatRelativeTime } from 'react-intl' import { selectUnit } from 'app/utils/selectUnit' import Tooltip from 'app/components/elements/Tooltip' +function processDate(date) { + if (date && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d$/.test(date)) { + date = date + 'Z' // Firefox really wants this Z (Zulu) + } + const dt = new Date(date) + const res = selectUnit(dt) + return { dt, res } +} + +export function wrapDate(date, intl) { + const { dt, res } = processDate(date) + const { value, unit } = res + return intl.formatRelativeTime(value, unit) +} + export default class TimeAgoWrapper extends React.Component { render() { - let {date, className} = this.props - if (date && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d$/.test(date)) { - date = date + 'Z' // Firefox really wants this Z (Zulu) - } - const dt = new Date(date) - const { value, unit } = selectUnit(dt) + const { date, className } = this.props + const { dt, res } = processDate(date) + const { value, unit } = res return - - + + } } diff --git a/app/components/modules/Header.jsx b/app/components/modules/Header.jsx index ebc946f3..98f6b675 100644 --- a/app/components/modules/Header.jsx +++ b/app/components/modules/Header.jsx @@ -113,6 +113,8 @@ class Header extends React.Component { page_title = tt('header_jsx.change_account_password'); } else if (route.page === 'MinusedAccounts') { page_title = tt('minused_accounts_jsx.title'); + } else if (route.page === 'Referrers') { + page_title = tt('referrers_jsx.title'); } else if (route.page === 'UserProfile') { user_name = route.params[0].slice(1); const acct_meta = this.props.account_meta.getIn([user_name]); @@ -125,6 +127,12 @@ class Header extends React.Component { if(route.params[1] === "followed"){ page_title = tt('header_jsx.people_followed_by') + " " + user_title; } + if (route.params[1] === "sponsors"){ + page_title = tt('sponsors_jsx.your_sponsors') + " " + user_title + } + if (route.params[1] === "referrals"){ + page_title = tt('referrals_jsx.title') + " " + user_title + } if(route.params[1] === "curation-rewards"){ page_title = tt('header_jsx.curation_rewards_by') + " " + user_title; } diff --git a/app/components/modules/Referrals.jsx b/app/components/modules/Referrals.jsx new file mode 100644 index 00000000..e5c70b75 --- /dev/null +++ b/app/components/modules/Referrals.jsx @@ -0,0 +1,192 @@ +import React from 'react' +import { Link } from 'react-router' +import { connect } from 'react-redux' +import tt from 'counterpart' +import { Asset } from 'golos-lib-js/lib/utils' +import CopyToClipboard from 'react-copy-to-clipboard' +import { IntlContext } from 'react-intl' + +import DateJoinWrapper from 'app/components/elements/DateJoinWrapper' +import DropdownMenu from 'app/components/elements/DropdownMenu' +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import TimeAgoWrapper, { wrapDate } from 'app/components/elements/TimeAgoWrapper' +import g from 'app/redux/GlobalReducer' +import LinkEx from 'app/utils/LinkEx' +import { getLastSeen } from 'app/utils/NormalizeProfile' +import { vestsToSteem } from 'app/utils/StateFunctions' + +class Referrals extends React.Component { + static contextType = IntlContext + + constructor(props, context) { + super(props, context) + } + + state = { + } + + componentDidMount() { + this.refetch() + } + + refetch = () => { + this.props.fetchReferrals(this.props.account, '', this.sort) + } + + sortOrder = (e, sort) => { + e.preventDefault() + this.sort = sort + this.refetch() + } + + render() { + const { referrals, account } = this.props + + const props = this.props.props ? this.props.props.toJS() : {} + + if (!referrals || !referrals.get('loaded')){ + return
+

{tt('referrals_jsx.title')}

+ +
+ } + + let refs = referrals.get('data').toJS() + + let count = 0 + + let items = refs.map(ref => { + ++count + + const { accounts } = this.props + let acc = accounts.get(ref.account) + if (acc) acc = acc.toJS() + + let lastSeen + if (acc) { + lastSeen = getLastSeen(acc) + } + + const golosRewards = Asset(ref.referrer_rewards).plus(Asset(ref.referrer_donate_rewards)) + + const inactive = acc && acc.referral_end_date.startsWith('19') + + const tdClass = inactive && 'inactive' + + return + + + {ref.account} + + + {acc && + {Asset(vestsToSteem(acc.vesting_shares, props) + ' GOLOS').floatString} + } + {acc && + {tt('user_profile.post_count', {count: acc.post_count || 0})} + } + + + {' +'} + {golosRewards.floatString} + + + + + + + {lastSeen && } + + + }) + + let refUrl + if (account) { + refUrl = 'https://' + $STM_Config.site_domain + '/welcome?invite=' + account.name + } + + const next_start_name = referrals.get('next_start_name') + + const sortItems = [ + { link: '#', onClick: e => { + this.sortOrder(e, 'by_joined', false) + }, value: tt('referrals_jsx.by_joined') }, + { link: '#', onClick: e => { + this.sortOrder(e, 'by_rewards', true) + }, value: tt('referrals_jsx.by_rewards') }, + ] + + let currentSort = tt('referrals_jsx.by_joined') + if (this.sort === 'by_rewards') { + currentSort = tt('referrals_jsx.by_rewards') + } + + return
+

{tt('referrals_jsx.title')}

+ + {tt('referrers_jsx.button')} + +
{tt('referrals_jsx.desc')}
+ {refUrl &&
+ + {' '}{tt('g.referral_link')}{' - '} + + {refUrl} + + + + + + +    +
} +
+ + + {currentSort} + + + +
+ {items.length ? + + + + + + + + + + {items} +
{tt('referrals_jsx.name')}{tt('referrals_jsx.vs')}{tt('referrals_jsx.posts')}{tt('referrals_jsx.rewards')}{tt('referrals_jsx.joined')}{tt('referrals_jsx.last')}
: null} + {next_start_name ?
+
+
: null} +
+ } +} + + +export default connect( + state => { + const referrals = state.global.get('referrals') + const accounts = state.global.get('accounts') + const props = state.global.get('props') + + return { + referrals, + accounts, + props, + } + }, + dispatch => ({ + fetchReferrals: (referrer, start_name, sort) => { + if (!referrer) return + dispatch(g.actions.fetchReferrals({ referrer: referrer.name, start_name, sort })) + }, + }) +)(Referrals) diff --git a/app/components/modules/Referrals.scss b/app/components/modules/Referrals.scss new file mode 100644 index 00000000..5f7621c3 --- /dev/null +++ b/app/components/modules/Referrals.scss @@ -0,0 +1,10 @@ +.Referrals { + .inactive-link { + a { + color: gray; + } + } + .inactive { + opacity: 0.5; + } +} diff --git a/app/components/modules/TopRightMenu.jsx b/app/components/modules/TopRightMenu.jsx index 57b5bce0..8b6af833 100644 --- a/app/components/modules/TopRightMenu.jsx +++ b/app/components/modules/TopRightMenu.jsx @@ -153,7 +153,7 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, if (loggedIn) { // change back to if(username) after bug fix: Clicking on Login does not cause drop-down to close #TEMP! let user_menu = [ {link: feedLink, icon: 'new/home', value: tt('g.feed'), addon: }, - {link: accountLink, icon: 'new/blogging', value: tt('g.blog'), addon: }, + {link: accountLink, icon: 'new/blogging', value: tt('g.blog'), addon: }, {link: repliesLink, icon: 'new/answer', value: tt('g.replies'), addon: }, {link: discussionsLink, icon: 'new/bell', value: tt('g.discussions'), addon: }, {link: mentionsLink, icon: 'new/mention', value: tt('g.mentions'), addon: }, @@ -199,7 +199,7 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops, -
+
} {navAdditional} diff --git a/app/components/pages/Referrers.jsx b/app/components/pages/Referrers.jsx new file mode 100644 index 00000000..3ce5570c --- /dev/null +++ b/app/components/pages/Referrers.jsx @@ -0,0 +1,158 @@ +import React from 'react' +import { connect } from 'react-redux' +import tt from 'counterpart' +import { Asset } from 'golos-lib-js/lib/utils' + +import DropdownMenu from 'app/components/elements/DropdownMenu' +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import g from 'app/redux/GlobalReducer' +import { vestsToSteem } from 'app/utils/StateFunctions' +import LinkEx from 'app/utils/LinkEx' + +class Referrers extends React.Component { + constructor(props) { + super(props) + } + + state = { + } + + componentDidMount() { + this.refetch() + } + + refetch = () => { + this.props.fetchReferrers('', this.sort) + } + + sortOrder = (e, sort) => { + e.preventDefault() + this.sort = sort + this.refetch() + } + + render() { + const { referrers } = this.props + + const props = this.props.props ? this.props.props.toJS() : {} + + if (!referrers || !referrers.get('loaded')){ + return
+
+
+

{tt('referrers_jsx.title')}

+ +
+
+
+ } + + let refs = referrers.get('data').toJS() + + let count = 0 + + let items = refs.map(ref => { + ++count + + const golosRewards = Asset(ref.referrer_rewards).plus(Asset(ref.referrer_donate_rewards)) + + const tdClass = '' + + return + + {count} + + + + {ref.account} + + + + + {tt('user_profile.referral_count', {count: ref.referral_count || 0})} + + + + {tt('user_profile.post_count', {count: ref.referral_post_count || 0})} + + + {Asset(vestsToSteem(ref.total_referral_vesting, props) + ' GOLOS').floatString} + + + }) + + const next_start_name = referrers.get('next_start_name') + + const sortItems = [ + { link: '#', onClick: e => { + this.sortOrder(e, 'by_referral_count', false) + }, value: tt('referrers_jsx.by_referral_count') }, + { link: '#', onClick: e => { + this.sortOrder(e, 'by_referral_vesting', true) + }, value: tt('referrers_jsx.by_referral_vesting') }, + { link: '#', onClick: e => { + this.sortOrder(e, 'by_referral_post_count', true) + }, value: tt('referrers_jsx.by_referral_post_count') }, + ] + + let currentSort = tt('referrers_jsx.by_referral_count') + if (this.sort === 'by_referral_vesting') { + currentSort = tt('referrers_jsx.by_referral_vesting') + } else if (this.sort === 'by_referral_post_count') { + currentSort = tt('referrers_jsx.by_referral_post_count') + } + + return
+
+
+

{tt('referrers_jsx.title')}

+
+ + + {currentSort} + + + +
+ {items.length ? + + + + + + + + + {items} +
{tt('referrals_jsx.name')}{tt('referrers_jsx.referral_count')}{tt('referrals_jsx.posts')}{tt('referrals_jsx.vs')}
: null} + {next_start_name ?
+
+
: null} +
+
+
+ } +} + +module.exports = { + path: '/referrers', + component: connect( + state => { + const referrers = state.global.get('referrers') + const props = state.global.get('props') + + return { + referrers, + props + } + }, + dispatch => ({ + fetchReferrers: (start_name, sort) => { + dispatch(g.actions.fetchReferrers({ start_name, sort })) + }, + }) + )(Referrers) +} diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 4b958f80..19bc741e 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -14,6 +14,7 @@ import UserKeys from 'app/components/elements/UserKeys'; import Settings from 'app/components/modules/Settings'; import ReputationHistory from 'app/components/modules/ReputationHistory' import Mentions from 'app/components/modules/Mentions' +import Referrals from 'app/components/modules/Referrals' import Sponsors from 'app/components/modules/Sponsors' import UserList from 'app/components/elements/UserList'; import Follow from 'app/components/elements/Follow'; @@ -398,6 +399,11 @@ export default class UserProfile extends React.Component { + } else if (section === 'referrals') { + tab_content =
+ + +
} tab_content =
@@ -514,6 +520,9 @@ export default class UserProfile extends React.Component { {tt('user_profile.sponsor_count', {count: account.sponsor_count || 0})} {isMyAccount && } + {tt('user_profile.referral_count', {count: account.referral_count || 0})} + {isMyAccount && } +

{location && {location}} diff --git a/app/locales/en.json b/app/locales/en.json index 6c910d9b..e6f75d33 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -384,6 +384,31 @@ "responses": "responses", "popular": "popular" }, + "referrals_jsx": { + "title": "Referrals", + "joined": "Registration", + "last": "Last activity", + "vs": "Golos Power", + "posts": "Post count", + "rewards": "Rewards and donates for referrer", + "donates_uia": "Donates in UIA", + "desc": "During the year, 10%% of rewards of users (registered by your referral link or invite codes) will be arrive to your TIP-balance (donates) and Golos Power (author rewards).", + "name": "Name", + "inactive": "Referral ended", + "end_date": "Referral witl be end ", + "by_joined": "By joined", + "by_rewards": "By rewards" + }, + "referrers_jsx": { + "button": "View top referrers", + "title": "Top referrers", + "referral_count": "Referral count", + "post_count": "Post count (by referrals)", + "vs": "Golos Power of referrals", + "by_referral_count": "By referral count", + "by_referral_vesting": "By referral Golos Power", + "by_referral_post_count": "By referral post count" + }, "sponsorslist_jsx": { "payment": "Payment", "payment_hint": "Next payment, will be claimed automatically", @@ -524,6 +549,11 @@ "one": "1 sponsor", "other": "%(count)s sponsors" }, + "referral_count": { + "zero": "No referrals", + "one": "1 referral", + "other": "%(count)s referrals" + }, "account_frozen": "Account is temporarily deactivated." }, "plurals": { diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json index fdabe2b5..5887bd60 100644 --- a/app/locales/ru-RU.json +++ b/app/locales/ru-RU.json @@ -782,6 +782,31 @@ "sponsored_authors": "Спонсируемые авторы", "created": "Создано" }, + "referrals_jsx": { + "title": "Рефералы", + "joined": "Регистрация", + "last": "Последняя активность", + "vs": "Сила Голоса", + "posts": "Кол-во постов", + "rewards": "Выплаты и донаты рефереру", + "donates_uia": "Донаты рефереру в UIA", + "desc": "В течение года 10%% от вознаграждений пользователей (зарегистрированных по вашей реферальной ссылке или инвайт-чеку) будут поступать на ваш TIP-баланс (донаты) и Силу Голоса (авторские награды).", + "name": "Имя", + "inactive": "Реферальство закончилось", + "end_date": "Реферальство закончится ", + "by_joined": "Сначала новые", + "by_rewards": "По наградам" + }, + "referrers_jsx": { + "button": "Смотреть топ рефереров", + "title": "Топ рефереров", + "referral_count": "Кол-во рефералов", + "post_count": "Кол-во постов (созданных рефералами)", + "vs": "Сила Голоса рефералов", + "by_referral_count": "По кол-ву рефералов", + "by_referral_vesting": "По Силе Голоса рефералов", + "by_referral_post_count": "По кол-ву постов рефералов" + }, "sponsorslist_jsx": { "payment": "Платеж", "payment_hint": "Следующий платеж, спишется автоматически", @@ -898,6 +923,11 @@ "one": "1 спонсор", "other": "%(count)s спонсоров" }, + "referral_count": { + "zero": "0 рефералов", + "one": "1 реферал", + "other": "%(count)s рефералов" + }, "account_frozen": "Аккаунт временно деактивирован." }, "invites_jsx": { diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index 8ebe6e87..2cfaf125 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -8,6 +8,7 @@ import {loadFollows, fetchFollowCount} from 'app/redux/FollowSaga'; import { getBlockings, listBlockings } from 'app/redux/BlockingSaga' import { contentPrefs as prefs } from 'app/utils/Allowance' import { applyEventHighlight, getContent } from 'app/redux/SagaShared' +import user from 'app/redux/User' import GlobalReducer from './GlobalReducer'; import constants from './constants'; import session from 'app/utils/session' @@ -30,6 +31,8 @@ export function* fetchDataWatches () { yield fork(watchFetchVestingDelegations); yield fork(watchFetchUiaBalances); yield fork(watchFetchNftTokens) + yield fork(watchFetchReferrals) + yield fork(watchFetchReferrers) } export function* watchGetContent() { @@ -283,6 +286,11 @@ export function* fetchState(location_change_action) { } break + case 'referrals': + state.referrals = { data: [] } + state.props = yield call([api, api.getDynamicGlobalProperties]) + break + case 'blog': default: const blogEntries = yield call([api, api.getBlogEntriesAsync], uname, 0, 20, ['fm-'], {}) @@ -443,6 +451,9 @@ export function* fetchState(location_change_action) { state.minused_accounts.push(operation); } }); + } else if (parts[0] === 'referrers') { + state.referrers = { data: [] } + state.props = yield call([api, api.getDynamicGlobalProperties]) } else if (Object.keys(PUBLIC_API).includes(parts[0])) { yield call(fetchData, {payload: { order: parts[0], category : tag }}) @@ -897,3 +908,61 @@ export function* fetchNftTokens({ payload: { account, start_token_id } }) { console.error('fetchNftTokens', err) } } + +export function* watchFetchReferrals() { + yield takeLatest('global/FETCH_REFERRALS', fetchReferrals) +} + +export function* fetchReferrals({ payload: { referrer, start_name, sort } }) { + try { + const limit = 20 + + const referrals = yield call([api, api.getReferralsAsync], { + referrer, + start_name, + limit: limit + 1, + sort: sort || 'by_joined', + }) + + const usernames = new Set() + for (const referral of referrals) { + usernames.add(referral.account) + } + + yield put(user.actions.getAccount({ usernames: [...usernames], })) + + let next_start_name + if (referrals.length > limit) { + next_start_name = referrals.pop().account + } + + yield put(GlobalReducer.actions.receiveReferrals({ referrals, start_name, next_start_name })) + } catch (err) { + console.error('fetchReferrals', err) + } +} + +export function* watchFetchReferrers() { + yield takeLatest('global/FETCH_REFERRERS', fetchReferrers) +} + +export function* fetchReferrers({ payload: { start_name, sort } }) { + try { + const limit = 20 + + const referrers = yield call([api, api.getReferrersAsync], { + start_name, + limit: limit + 1, + sort: sort || 'by_referral_count', + }) + + let next_start_name + if (referrers.length > limit) { + next_start_name = referrers.pop().account + } + + yield put(GlobalReducer.actions.receiveReferrers({ referrers, start_name, next_start_name })) + } catch (err) { + console.error('fetchReferrers', err) + } +} diff --git a/app/redux/GlobalReducer.js b/app/redux/GlobalReducer.js index 6317daa9..f848a65b 100644 --- a/app/redux/GlobalReducer.js +++ b/app/redux/GlobalReducer.js @@ -72,6 +72,12 @@ export default createModule({ res = res.delete('pso') } res = res.setIn(['sponsoreds', 'data'], List()) + if (!payload.has('referrals')) { + res = res.delete('referrals') + } + if (!payload.has('referrers')) { + res = res.delete('referrers') + } if (res.has('nft_tokens')) res = res.delete('nft_tokens') res = res.mergeDeep(payload); @@ -235,6 +241,64 @@ export default createModule({ return new_state }, }, + { + action: 'FETCH_REFERRALS', + reducer: state => state, + }, + { + action: 'RECEIVE_REFERRALS', + reducer: (state, { payload: { referrals, start_name, next_start_name } }) => { + let new_state = state + if (!start_name) { + new_state = new_state.set('referrals', fromJS({ + data: referrals, + next_start_name, + loaded: true, + })) + } else { + new_state = new_state.update('referrals', refs => { + refs = refs.update('data', data => { + for (const referral of referrals) { + data = data.push(fromJS(referral)) + } + return data + }) + refs = refs.set('next_start_name', next_start_name) + return refs + }) + } + return new_state + }, + }, + { + action: 'FETCH_REFERRERS', + reducer: state => state, + }, + { + action: 'RECEIVE_REFERRERS', + reducer: (state, { payload: { referrers, start_name, next_start_name } }) => { + let new_state = state + if (!start_name) { + new_state = new_state.set('referrers', fromJS({ + data: referrers, + next_start_name, + loaded: true, + })) + } else { + new_state = new_state.update('referrers', refs => { + refs = refs.update('data', data => { + for (const referrer of referrers) { + data = data.push(fromJS(referrer)) + } + return data + }) + refs = refs.set('next_start_name', next_start_name) + return refs + }) + } + return new_state + }, + }, { action: 'LINK_REPLY', reducer: (state, { payload: op }) => { diff --git a/package.json b/package.json index 629c949f..be5680ff 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "foundation-sites": "^6.4.3", "fs-extra": "^10.0.1", "git-rev-sync": "^3.0.2", - "golos-lib-js": "^0.9.56", + "golos-lib-js": "^0.9.61", "history": "^2.0.0-rc2", "immutable": "^3.8.2", "intl": "^1.2.5", @@ -89,6 +89,7 @@ "react": "^18.2.0", "react-addons-pure-render-mixin": "^15.6.3", "react-cookie": "1.0.4", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-dom-confetti": "^0.1.3", "react-dropzone": "^4.2.12", diff --git a/yarn.lock b/yarn.lock index 23a8776e..36ac0f8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2528,6 +2528,15 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -3159,6 +3168,13 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-to-clipboard@^3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + core-js-compat@^3.18.0, core-js-compat@^3.19.0: version "3.19.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.1.tgz#fe598f1a9bf37310d77c3813968e9f7c7bb99476" @@ -3177,9 +3193,9 @@ core-js@^2.4.0, core-js@^2.5.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" core-js@^3.17.3: - version "3.32.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.2.tgz#172fb5949ef468f93b4be7841af6ab1f21992db7" - integrity sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ== + version "3.33.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.3.tgz#3c644a323f0f533a0d360e9191e37f7fc059088d" + integrity sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw== core-js@^3.19.0, core-js@^3.19.1: version "3.19.1" @@ -3753,10 +3769,10 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-data-property@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" - integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== dependencies: get-intrinsic "^1.2.1" gopd "^1.0.1" @@ -4993,6 +5009,11 @@ function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -5054,15 +5075,15 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-port@^3.2.0: version "3.2.0" @@ -5182,10 +5203,10 @@ globule@^1.0.0: lodash "^4.17.21" minimatch "~3.0.2" -golos-lib-js@^0.9.56: - version "0.9.56" - resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.56.tgz#3dfe8c0658fba2f50976ef49103ddc3f34109c19" - integrity sha512-h9ay0q2AuHiYL8aFXsCGoEFe6ojHt67FHMv8W6oWbqayl44JlRuuEysfE1MZQiiLwzBDFOO1SNMAtv5sE0bRcg== +golos-lib-js@^0.9.61: + version "0.9.61" + resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.61.tgz#d54d5f5dc66eaa42b78047588903930eb8b029a8" + integrity sha512-OEZC/zov/Ur76UkI/AdieHWypSaqwWtcGWSccYzx/x/DaxMv+ExpdnmXrae7+OPmTur2Kj6f8/JhfZ1tgdgllg== dependencies: abort-controller "^3.0.0" assert "^2.0.0" @@ -5306,11 +5327,11 @@ has-flag@^4.0.0: integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== dependencies: - get-intrinsic "^1.1.1" + get-intrinsic "^1.2.2" has-proto@^1.0.1: version "1.0.1" @@ -5391,6 +5412,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + he@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -9019,7 +9047,7 @@ prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.6.1: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.5.4, prop-types@^15.5.8: +prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -9225,6 +9253,14 @@ react-cookie@1.0.4: cookie "^0.3.1" object-assign "^4.1.0" +react-copy-to-clipboard@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" + integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A== + dependencies: + copy-to-clipboard "^3.3.1" + prop-types "^15.8.1" + react-dom-confetti@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/react-dom-confetti/-/react-dom-confetti-0.1.4.tgz#64025805f58eecd58ee8184e3d0ad3fc25ff3aee" @@ -10154,6 +10190,16 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + set-value@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" @@ -11022,6 +11068,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" @@ -11851,12 +11902,12 @@ which-boxed-primitive@^1.0.2: is-symbol "^1.0.3" which-typed-array@^1.1.11, which-typed-array@^1.1.2: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== dependencies: available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + call-bind "^1.0.4" for-each "^0.3.3" gopd "^1.0.1" has-tostringtag "^1.0.0"