utils#slugify TypeScript Examples
The following examples show how to use
utils#slugify.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: summary_row.tsx From commonwealth with GNU General Public License v3.0 | 6 votes |
getThreadCells = (sortedThreads: OffchainThread[]) => {
return sortedThreads.slice(0, 3).map((thread) => {
const discussionLink = getProposalUrlPath(
thread.slug,
`${thread.identifier}-${slugify(thread.title)}`
);
return (
<div class="thread-summary">
{link('a.thread-title', discussionLink, thread.title)}
<div class="last-updated">
{formatTimestampAsDate(getLastUpdated(thread))}
{isHot(thread) && <span>?</span>}
</div>
</div>
);
});
}
Example #2
Source File: header.tsx From commonwealth with GNU General Public License v3.0 | 5 votes |
ProposalTitleSaveEdit: m.Component<{
proposal: AnyProposal;
getSetGlobalEditingStatus;
parentState;
}> = {
view: (vnode) => {
const { proposal, getSetGlobalEditingStatus, parentState } = vnode.attrs;
if (!proposal) return;
const proposalLink = getProposalUrlPath(
proposal.slug,
`${proposal.identifier}-${slugify(proposal.title)}`
);
return m('.ProposalTitleSaveEdit', [
m(
Button,
{
class: 'save-editing',
label: 'Save',
disabled: parentState.saving,
intent: 'primary',
rounded: true,
onclick: (e) => {
e.preventDefault();
parentState.saving = true;
app.chain.chainEntities
.updateEntityTitle(
proposal.uniqueIdentifier,
parentState.updatedTitle
)
.then((response) => {
m.route.set(proposalLink);
parentState.editing = false;
parentState.saving = false;
getSetGlobalEditingStatus(GlobalStatus.Set, false);
proposal.title = parentState.updatedTitle;
m.redraw();
notifySuccess('Thread successfully edited');
});
},
},
'Save'
),
]);
},
}
Example #3
Source File: proposal_card.tsx From commonwealth with GNU General Public License v3.0 | 4 votes |
view(vnode) {
const { proposal, injectedContent } = vnode.attrs;
return (
<CWCard
elevation="elevation-2"
interactive={true}
className="ProposalCard"
onclick={(e) => {
e.stopPropagation();
e.preventDefault();
localStorage[`${app.activeChainId()}-proposals-scrollY`] =
window.scrollY;
const path = getProposalUrlPath(
proposal.slug,
`${proposal.identifier}-${slugify(proposal.title)}`,
true
);
navigateToSubpage(path); // avoid resetting scroll point
}}
>
<div class="proposal-card-metadata">
<Tag
label={[
chainEntityTypeToProposalShortName(
proposalSlugToChainEntityType(proposal.slug)
),
' ',
proposal.shortIdentifier,
]}
intent="primary"
rounded={true}
size="xs"
/>
{(proposal instanceof SubstrateDemocracyProposal ||
proposal instanceof SubstrateCollectiveProposal) &&
proposal.getReferendum() && (
<Tag
label={`REF #${proposal.getReferendum().identifier}`}
intent="primary"
rounded={true}
size="xs"
class="proposal-became-tag"
/>
)}
{proposal instanceof SubstrateDemocracyReferendum &&
(() => {
const originatingProposalOrMotion = proposal.getProposalOrMotion(
proposal.preimage
);
return (
<Tag
label={
originatingProposalOrMotion instanceof
SubstrateDemocracyProposal
? `PROP #${originatingProposalOrMotion.identifier}`
: originatingProposalOrMotion instanceof
SubstrateCollectiveProposal
? `MOT #${originatingProposalOrMotion.identifier}`
: 'MISSING PROP'
}
intent="primary"
rounded={true}
size="xs"
class="proposal-became-tag"
/>
);
})()}
{proposal instanceof SubstrateTreasuryProposal &&
!proposal.data.index && (
<Tag
label="MISSING DATA"
intent="primary"
rounded={true}
size="xs"
class="proposal-became-tag"
/>
)}
<div class="proposal-title" title={proposal.title}>
{proposal.title}
</div>
{proposal instanceof SubstrateTreasuryProposal && (
<div class="proposal-amount">{proposal.value?.format(true)}</div>
)}
{proposal instanceof SubstrateDemocracyReferendum && (
<div class="proposal-amount">{proposal.threshold}</div>
)}
{proposal instanceof AaveProposal && (
<p class="card-subheader">
{proposal.ipfsData?.shortDescription || 'Proposal'}
</p>
)}
{/* linked treasury proposals
proposal instanceof SubstrateDemocracyReferendum && proposal.preimage?.section === 'treasury'
&& proposal.preimage?.method === 'approveProposal'
&& m('.proposal-action', [ 'Approves TRES-', proposal.preimage?.args[0] ]),
proposal instanceof SubstrateDemocracyProposal && proposal.preimage?.section === 'treasury'
&& proposal.preimage?.method === 'approveProposal'
&& m('.proposal-action', [ 'Approves TRES-', proposal.preimage?.args[0] ]),
proposal instanceof SubstrateCollectiveProposal && proposal.call?.section === 'treasury'
&& proposal.call?.method === 'approveProposal'
&& m('.proposal-action', [ 'Approves TRES-', proposal.call?.args[0] ]),
linked referenda */}
</div>
{injectedContent ? (
<div class="proposal-injected">
{m(injectedContent, {
proposal,
statusClass: getStatusClass(proposal),
statusText: getStatusText(proposal),
})}
</div>
) : proposal.isPassing !== 'none' ? (
<div class={`proposal-status ${getStatusClass(proposal)}`}>
{getStatusText(proposal)}
</div>
) : null}
{proposal.threadId && (
<div class="proposal-thread-link">
<a
href={getProposalUrlPath(
ProposalType.OffchainThread,
`${proposal.threadId}`
)}
onclick={(e) => {
e.stopPropagation();
e.preventDefault();
localStorage[`${app.activeChainId()}-proposals-scrollY`] =
window.scrollY;
navigateToSubpage(
getProposalUrlPath(
ProposalType.OffchainThread,
`${proposal.threadId}`,
true
)
);
// avoid resetting scroll point
}}
>
<CWIcon iconName="expand" iconSize="small" />
<span>
{proposal.threadTitle ? proposal.threadTitle : 'Go to thread'}
</span>
</a>
</div>
)}
</CWCard>
);
}
Example #4
Source File: erc20_form.tsx From commonwealth with GNU General Public License v3.0 | 4 votes |
view(vnode) {
const validAddress = isAddress(this.state.form.address);
const disableField = !validAddress || !this.state.loaded;
const updateTokenForum = async () => {
if (!this.state.form.address || !this.state.form.ethChainId) return;
this.state.status = '';
this.state.error = '';
this.state.loading = true;
const args = {
address: this.state.form.address,
chain_id: this.state.form.ethChainId,
chain_network: ChainNetwork.ERC20,
url: this.state.form.nodeUrl,
allowUncached: true,
};
try {
console.log('Querying backend for token data');
const res = await $.get(`${app.serverUrl()}/getTokenForum`, args);
if (res.status === 'Success') {
if (res?.token?.name) {
this.state.form.name = res.token.name || '';
this.state.form.id = res.token.id && slugify(res.token.id);
this.state.form.symbol = res.token.symbol || '';
this.state.form.decimals = +res.token.decimals || 18;
this.state.form.iconUrl = res.token.icon_url || '';
if (this.state.form.iconUrl.startsWith('/')) {
this.state.form.iconUrl = `https://commonwealth.im${this.state.form.iconUrl}`;
}
this.state.form.description = res.token.description || '';
this.state.form.website = res.token.website || '';
this.state.form.discord = res.token.discord || '';
this.state.form.element = res.token.element || '';
this.state.form.telegram = res.token.telegram || '';
this.state.form.github = res.token.github || '';
this.state.status = 'Success!';
} else {
// attempt to query ERC20Detailed token info from chain
console.log('Querying chain for ERC info');
const provider = new Web3.providers.WebsocketProvider(args.url);
try {
const ethersProvider = new providers.Web3Provider(provider);
const contract = IERC20Metadata__factory.connect(
args.address,
ethersProvider
);
const name = await contract.name();
const symbol = await contract.symbol();
const decimals = await contract.decimals();
this.state.form.name = name || '';
this.state.form.id = name && slugify(name);
this.state.form.symbol = symbol || '';
this.state.form.decimals = decimals || 18;
this.state.status = 'Success!';
} catch (e) {
this.state.form.name = '';
this.state.form.id = '';
this.state.form.symbol = '';
this.state.form.decimals = 18;
this.state.status = 'Verified token but could not load metadata.';
}
this.state.form.iconUrl = '';
this.state.form.description = '';
this.state.form.website = '';
this.state.form.discord = '';
this.state.form.element = '';
this.state.form.telegram = '';
this.state.form.github = '';
provider.disconnect(1000, 'finished');
}
this.state.loaded = true;
} else {
this.state.error = res.message || 'Failed to load Token Information';
}
} catch (err) {
this.state.error =
err.responseJSON?.error || 'Failed to load Token Information';
}
this.state.loading = false;
m.redraw();
};
return (
<div class="CreateCommunityForm">
{...ethChainRows(vnode.attrs, this.state.form)}
<CWButton
label="Populate fields"
disabled={
this.state.saving ||
!validAddress ||
!this.state.form.ethChainId ||
this.state.loading
}
onclick={async () => {
await updateTokenForum();
}}
/>
<ValidationRow error={this.state.error} status={this.state.status} />
<InputRow
title="Name"
defaultValue={this.state.form.name}
disabled={disableField}
onChangeHandler={(v) => {
this.state.form.name = v;
this.state.form.id = slugifyPreserveDashes(v);
}}
/>
<IdRow id={this.state.form.id} />
<InputRow
title="Symbol"
disabled={disableField}
defaultValue={this.state.form.symbol}
placeholder="XYZ"
onChangeHandler={(v) => {
this.state.form.symbol = v;
}}
/>
{...defaultChainRows(this.state.form, disableField)}
<CWButton
label="Save changes"
disabled={this.state.saving || !validAddress || !this.state.loaded}
onclick={async () => {
const { altWalletUrl, chainString, ethChainId, nodeUrl } =
this.state.form;
this.state.saving = true;
mixpanelBrowserTrack({
event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
chainBase: null,
isCustomDomain: app.isCustomDomain(),
communityType: null,
});
try {
const res = await $.post(`${app.serverUrl()}/createChain`, {
alt_wallet_url: altWalletUrl,
base: ChainBase.Ethereum,
chain_string: chainString,
eth_chain_id: ethChainId,
jwt: app.user.jwt,
network: ChainNetwork.ERC20,
node_url: nodeUrl,
type: ChainType.Token,
...this.state.form,
});
await initAppState(false);
m.route.set(`/${res.result.chain?.id}`);
} catch (err) {
notifyError(
err.responseJSON?.error || 'Creating new ERC20 community failed'
);
} finally {
this.state.saving = false;
}
}}
/>
</div>
);
}
Example #5
Source File: erc721_form.tsx From commonwealth with GNU General Public License v3.0 | 4 votes |
view(vnode) {
const validAddress = isAddress(this.state.form.address);
const disableField = !validAddress || !this.state.loaded;
const updateTokenForum = async () => {
if (!this.state.form.address || !this.state.form.ethChainId) return;
this.state.status = '';
this.state.error = '';
this.state.loading = true;
const args = {
address: this.state.form.address,
chain_id: this.state.form.ethChainId,
chain_network: ChainNetwork.ERC721,
url: this.state.form.nodeUrl,
allowUncached: true,
};
try {
console.log('Querying backend for token data');
const res = await $.get(`${app.serverUrl()}/getTokenForum`, args);
if (res.status === 'Success') {
if (res?.token?.name) {
this.state.form.name = res.token.name || '';
this.state.form.id = res.token.id && slugify(res.token.id);
this.state.form.symbol = res.token.symbol || '';
this.state.form.iconUrl = res.token.icon_url || '';
if (this.state.form.iconUrl.startsWith('/')) {
this.state.form.iconUrl = `https://commonwealth.im${this.state.form.iconUrl}`;
}
this.state.form.description = res.token.description || '';
this.state.form.website = res.token.website || '';
this.state.form.discord = res.token.discord || '';
this.state.form.element = res.token.element || '';
this.state.form.telegram = res.token.telegram || '';
this.state.form.github = res.token.github || '';
this.state.status = 'Success!';
} else {
// attempt to query ERC721Detailed token info from chain
console.log('Querying chain for ERC info');
const provider = new Web3.providers.WebsocketProvider(args.url);
try {
const ethersProvider = new providers.Web3Provider(provider);
const contract = IERC721Metadata__factory.connect(
args.address,
ethersProvider
);
const name = await contract.name();
const symbol = await contract.symbol();
const decimals = await contract.decimals();
this.state.form.name = name || '';
this.state.form.id = name && slugify(name);
this.state.form.symbol = symbol || '';
this.state.status = 'Success!';
} catch (e) {
this.state.form.name = '';
this.state.form.id = '';
this.state.form.symbol = '';
this.state.status = 'Verified token but could not load metadata.';
}
this.state.form.iconUrl = '';
this.state.form.description = '';
this.state.form.website = '';
this.state.form.discord = '';
this.state.form.element = '';
this.state.form.telegram = '';
this.state.form.github = '';
provider.disconnect(1000, 'finished');
}
this.state.loaded = true;
} else {
this.state.error = res.message || 'Failed to load Token Information';
}
} catch (err) {
this.state.error =
err.responseJSON?.error || 'Failed to load Token Information';
}
this.state.loading = false;
m.redraw();
};
return (
<div class="CreateCommunityForm">
{...ethChainRows(vnode.attrs, this.state.form)}
<CWButton
label="Populate fields"
disabled={
this.state.saving ||
!validAddress ||
!this.state.form.ethChainId ||
this.state.loading
}
onclick={async () => {
await updateTokenForum();
}}
/>
<ValidationRow error={this.state.error} status={this.state.status} />
<InputRow
title="Name"
defaultValue={this.state.form.name}
disabled={disableField}
onChangeHandler={(v) => {
this.state.form.name = v;
this.state.form.id = slugifyPreserveDashes(v);
}}
/>
<IdRow id={this.state.form.id} />
<InputRow
title="Symbol"
disabled={disableField}
defaultValue={this.state.form.symbol}
placeholder="XYZ"
onChangeHandler={(v) => {
this.state.form.symbol = v;
}}
/>
{...defaultChainRows(this.state.form, disableField)}
<CWButton
label="Save changes"
disabled={this.state.saving || !validAddress || !this.state.loaded}
onclick={async () => {
const { altWalletUrl, chainString, ethChainId, nodeUrl } =
this.state.form;
this.state.saving = true;
mixpanelBrowserTrack({
event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
chainBase: null,
isCustomDomain: app.isCustomDomain(),
communityType: null,
});
try {
const res = await $.post(`${app.serverUrl()}/createChain`, {
alt_wallet_url: altWalletUrl,
base: ChainBase.Ethereum,
chain_string: chainString,
eth_chain_id: ethChainId,
jwt: app.user.jwt,
network: ChainNetwork.ERC721,
node_url: nodeUrl,
type: ChainType.Token,
...this.state.form,
});
await initAppState(false);
m.route.set(`/${res.result.chain?.id}`);
} catch (err) {
notifyError(
err.responseJSON?.error ||
'Creating new ERC721 community failed'
);
} finally {
this.state.saving = false;
}
}}
/>
</div>
);
}
Example #6
Source File: substrate_form.tsx From commonwealth with GNU General Public License v3.0 | 4 votes |
view() {
return (
<div class="CreateCommunityForm">
<InputRow
title="Name"
defaultValue={this.state.form.name}
onChangeHandler={(v) => {
this.state.form.name = v;
}}
/>
<InputRow
title="Node URL"
defaultValue={this.state.form.nodeUrl}
placeholder="wss://"
onChangeHandler={(v) => {
this.state.form.nodeUrl = v;
}}
/>
<InputRow
title="Symbol"
defaultValue={this.state.form.symbol}
placeholder="XYZ"
onChangeHandler={(v) => {
this.state.form.symbol = v;
}}
/>
<InputRow
title="Spec (JSON)"
defaultValue={this.state.form.substrateSpec}
// TODO: how to make this resizable vertically?
// looks like CUI specifies an !important height tag, which prevents this
textarea={true}
placeholder='{"types": {"Address": "MultiAddress", "ChainId": "u8", "Reveals": "Vec<(AccountId, Vec<VoteOutcome>)>", "Balance2": "u128", "VoteData": {"stage": "VoteStage", "initiator": "AccountId", "vote_type": "VoteType", "tally_type": "TallyType", "is_commit_reveal": "bool"}, "VoteType": {"_enum": ["Binary", "MultiOption", "RankedChoice"]}, "TallyType": {"_enum": ["OnePerson", "OneCoin"]}, "VoteStage": {"_enum": ["PreVoting", "Commit", "Voting", "Completed"]}, "ResourceId": "[u8; 32]", "VoteRecord": {"id": "u64", "data": "VoteData", "reveals": "Reveals", "outcomes": "Vec<VoteOutcome>", "commitments": "Commitments"}, "AccountInfo": "AccountInfoWithRefCount", "Commitments": "Vec<(AccountId, VoteOutcome)>", "VoteOutcome": "[u8; 32]", "VotingTally": "Option<Vec<(VoteOutcome, u128)>>", "DepositNonce": "u64", "LookupSource": "MultiAddress", "ProposalTitle": "Bytes", "ProposalVotes": {"staus": "ProposalStatus", "expiry": "BlockNumber", "votes_for": "Vec<AccountId>", "votes_against": "Vec<AccountId>"}, "ProposalRecord": {"index": "u32", "stage": "VoteStage", "title": "Text", "author": "AccountId", "vote_id": "u64", "contents": "Text", "transition_time": "u32"}, "ProposalStatus": {"_enum": ["Initiated", "Approved", "Rejected"]}, "ProposalContents": "Bytes"}}'
onChangeHandler={(v) => {
this.state.form.substrateSpec = v;
}}
/>
<CWButton
label="Test Connection"
className="button-margin-bottom"
onclick={async () => {
// deinit substrate API if one exists
if (app.chain?.apiInitialized) {
await app.chain.deinit();
}
// create new API
const provider = new WsProvider(
constructSubstrateUrl(this.state.form.nodeUrl),
false
);
try {
await provider.connect();
const api = await ApiPromise.create({
throwOnConnect: true,
provider,
...JSON.parse(this.state.form.substrateSpec),
});
await api.disconnect();
notifySuccess('Test has passed');
} catch (err) {
console.error(err.message);
notifyError('Test API initialization failed');
}
}}
/>
{...defaultChainRows(this.state.form)}
<CWButton
label="Save changes"
disabled={this.state.saving}
onclick={async () => {
const { name, nodeUrl, iconUrl, substrateSpec } = this.state.form;
mixpanelBrowserTrack({
event: MixpanelCommunityCreationEvent.CREATE_COMMUNITY_ATTEMPTED,
chainBase: null,
isCustomDomain: app.isCustomDomain(),
communityType: null,
});
try {
JSON.parse(substrateSpec);
} catch (err) {
notifyError('Spec provided has invalid JSON');
return;
}
this.state.saving = true;
$.post(`${app.serverUrl()}/createChain`, {
base: ChainBase.Substrate,
icon_url: iconUrl,
id: slugify(name),
jwt: app.user.jwt,
network: slugify(name),
node_url: nodeUrl,
substrate_spec: substrateSpec,
type: ChainType.Chain,
...this.state.form,
})
.then(async (res) => {
await initAppState(false);
m.route.set(`/${res.result.chain.id}`);
})
.catch((err: any) => {
notifyError(
err.responseJSON?.error || 'Creating new community failed'
);
})
.always(() => {
this.state.saving = false;
});
}}
/>
</div>
);
}
Example #7
Source File: discussion_row.tsx From commonwealth with GNU General Public License v3.0 | 4 votes |
view(vnode: m.VnodeDOM<DiscussionRowAttrs, this>) {
const { proposal } = vnode.attrs;
const discussionLink = getProposalUrlPath(
proposal.slug,
`${proposal.identifier}-${slugify(proposal.title)}`
);
return (
<div
class="DiscussionRow"
onclick={(e) => {
if (vnode.attrs.onSelect) {
return vnode.attrs.onSelect();
}
if ($(e.target).hasClass('cui-tag')) return;
if (e.metaKey || e.altKey || e.shiftKey || e.ctrlKey) {
window.open(discussionLink, '_blank');
return;
}
e.preventDefault();
const scrollEle = document.getElementsByClassName('Body')[0];
localStorage[`${app.activeChainId()}-discussions-scrollY`] =
scrollEle.scrollTop;
m.route.set(discussionLink);
}}
key={proposal.id}
>
{proposal.pinned ? (
<div class="pinned">
<CWIcon iconName="pin" iconSize="small" />
</div>
) : (
<DiscussionRowReactionButton thread={proposal} />
)}
<div class="title-container">
<div class="row-header">{proposal.title}</div>
<div class="row-subheader">
{proposal.readOnly && (
<div class="discussion-locked">
<Tag
size="xs"
label={<CWIcon iconName="lock" iconSize="small" />}
/>
</div>
)}
{proposal.hasPoll && (
<Button label="Poll" intent="warning" size="xs" compact={true} />
)}
{proposal.chainEntities?.length > 0 &&
proposal.chainEntities
.sort((a, b) => {
return +a.typeId - +b.typeId;
})
.map((ce) => {
if (!chainEntityTypeToProposalShortName(ce.type)) return;
return (
<Button
label={[
chainEntityTypeToProposalShortName(ce.type),
Number.isNaN(parseInt(ce.typeId, 10))
? ''
: ` #${ce.typeId}`,
]}
intent="primary"
class="proposal-button"
size="xs"
compact={true}
/>
);
})}
{proposal.snapshotProposal && (
<Button
label={['Snap ', `${proposal.snapshotProposal.slice(0, 4)}…`]}
intent="primary"
class="proposal-button"
size="xs"
compact={true}
/>
)}
{proposal.stage !== OffchainThreadStage.Discussion && (
<Button
intent={
proposal.stage === OffchainThreadStage.ProposalInReview
? 'positive'
: proposal.stage === OffchainThreadStage.Voting
? 'positive'
: proposal.stage === OffchainThreadStage.Passed
? 'positive'
: proposal.stage === OffchainThreadStage.Failed
? 'negative'
: 'positive'
}
size="xs"
compact={true}
label={offchainThreadStageToLabel(proposal.stage)}
/>
)}
{proposal.kind === OffchainThreadKind.Link &&
proposal.url &&
externalLink(
'a.external-discussion-link',
proposal.url,
`Link: ${extractDomain(proposal.url)}`
)}
{proposal.topic &&
link(
'a.proposal-topic',
`/${app.activeChainId()}/discussions/${proposal.topic.name}`,
<span class="proposal-topic-name">{proposal.topic.name}</span>
)}
{m(User, {
user: new AddressInfo(
null,
proposal.author,
proposal.authorChain,
null
),
linkify: true,
popover: false,
hideAvatar: true,
showAddressWithDisplayName: true,
hideIdentityIcon: true,
})}
{proposal.collaborators && proposal.collaborators.length > 0 && (
<span class="proposal-collaborators">
+{proposal.collaborators.length}
</span>
)}
<div class="last-active created-at">
{link(
'a',
discussionLink,
`Last active ${formatLastUpdated(getLastUpdated(proposal))}`
)}
</div>
{isHot(proposal) && (
<div class="activity-icons">
<span>?</span>
</div>
)}
</div>
</div>
<div class="content-right-container">
{m(UserGallery, {
avatarSize: 36,
popover: true,
maxUsers: 2,
addressesCount:
app.threadUniqueAddressesCount.getAddressesCountRootId(
`${proposal.slug}_${proposal.id}`
),
users:
app.threadUniqueAddressesCount.getUniqueAddressesByRootId(
proposal
),
})}
{app.isLoggedIn() && <DiscussionRowMenu proposal={proposal} />}
</div>
</div>
);
}
Example #8
Source File: profile_proposal.ts From commonwealth with GNU General Public License v3.0 | 4 votes |
ProfileProposal: m.Component<
{ proposal: OffchainThread },
{ revealThread: boolean }
> = {
view: (vnode) => {
const proposal = vnode.attrs.proposal;
const { slug, identifier } = proposal;
const { attachments, author, body, title, createdAt, chain } = proposal;
// hide rows from communities that don't match
if (app.isCustomDomain() && chain !== app.customDomainId()) return;
return m('.ProfileProposal', [
m('.summary', [
m('', [
proposal.kind === OffchainThreadKind.Question
? 'Added a question'
: proposal.kind === OffchainThreadKind.Request
? 'added a task'
: [
'Created a new ',
link(
'a.link-bold',
`/${chain}${getProposalUrlPath(
slug,
`${identifier}-${slugify(title)}`,
true
)}`,
'thread',
{},
`profile-${author}-${proposal.authorChain}-${proposal.chain}-scrollY`
),
' in ',
link('a.link-bold', `/${chain}`, `${chain}`),
],
]),
createdAt && createdAt.fromNow(),
]),
m('.activity.proposal', [
proposal.kind === OffchainThreadKind.Forum ||
proposal.kind === OffchainThreadKind.Link
? link(
'a.proposal-title',
`/${chain}${getProposalUrlPath(
slug,
`${identifier}-${slugify(title)}`,
true
)}`,
title,
{},
`profile-${author}-${proposal.authorChain}-${proposal.chain}-scrollY`
)
: m('a.proposal-title', title),
// TODO: show a truncated thread once we have a good formatting stripping helper
attachments &&
attachments.length > 0 &&
m('.proposal-attachments', [
m('p', `Attachments (${attachments.length})`),
attachments.map((attachment) =>
m(
'a.attachment-item',
{
href: attachment.url,
title: attachment.description,
target: '_blank',
noopener: 'noopener',
noreferrer: 'noreferrer',
onclick: (e) => {
e.preventDefault();
lity(attachment.url);
},
},
[
m('img', {
src: attachment.url,
}),
]
)
),
]),
]),
]);
},
}
Example #9
Source File: index.ts From commonwealth with GNU General Public License v3.0 | 4 votes |
ProposalHeader: m.Component<
{
commentCount: number;
viewCount: number;
getSetGlobalEditingStatus: CallableFunction;
proposalPageState: IProposalPageState;
proposal: AnyProposal | OffchainThread;
isAuthor: boolean;
isEditor: boolean;
isAdmin: boolean;
stageEditorIsOpen: boolean;
pollEditorIsOpen: boolean;
closePollEditor: Function;
closeStageEditor: Function;
},
{
savedEdit: string;
editing: boolean;
saving: boolean;
quillEditorState: any;
currentText: any;
topicEditorIsOpen: boolean;
editPermissionsIsOpen: boolean;
updatedTitle: string;
updatedUrl: string;
}
> = {
view: (vnode) => {
const {
commentCount,
proposal,
getSetGlobalEditingStatus,
proposalPageState,
viewCount,
isAuthor,
isEditor,
isAdmin,
} = vnode.attrs;
const attachments =
proposal instanceof OffchainThread
? (proposal as OffchainThread).attachments
: false;
const proposalLink = getProposalUrlPath(
proposal.slug,
`${proposal.identifier}-${slugify(proposal.title)}`
);
const proposalTitleIsEditable =
proposal instanceof SubstrateDemocracyProposal ||
proposal instanceof SubstrateCollectiveProposal ||
proposal instanceof SubstrateTreasuryTip ||
proposal instanceof SubstrateTreasuryProposal;
const hasBody = !!(proposal as AnyProposal).description;
return m(
'.ProposalHeader',
{
class: `proposal-${proposal.slug}`,
},
[
m('.proposal-top', [
m('.proposal-top-left', [
!(proposal instanceof OffchainThread) &&
m('.proposal-meta-top', [
m('.proposal-meta-top-left', [
m(ProposalHeaderOnchainId, { proposal }),
]),
m('.proposal-meta-top-right', [
m(QueueButton, { proposal }),
m(ExecuteButton, { proposal }),
m(CancelButton, { proposal }),
]),
]),
!vnode.state.editing &&
m('.proposal-title', [m(ProposalHeaderTitle, { proposal })]),
vnode.state.editing &&
m(ProposalTitleEditor, {
item: proposal,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
m(
'.proposal-body-meta',
proposal instanceof OffchainThread
? [
m(ProposalHeaderStage, { proposal }),
m(ProposalHeaderTopics, { proposal }),
m(ProposalBodyCreated, {
item: proposal,
link: proposalLink,
}),
m(ProposalBodyLastEdited, { item: proposal }),
m(ProposalBodyAuthor, { item: proposal }),
m(ProposalHeaderViewCount, { viewCount }),
app.isLoggedIn() &&
!getSetGlobalEditingStatus(GlobalStatus.Get) &&
m(PopoverMenu, {
transitionDuration: 0,
closeOnOutsideClick: true,
closeOnContentClick: true,
menuAttrs: { size: 'default' },
content: [
(isEditor || isAuthor || isAdmin) &&
m(ProposalBodyEditMenuItem, {
item: proposal,
proposalPageState: vnode.attrs.proposalPageState,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
isAuthor &&
m(EditPermissionsButton, {
openEditPermissions: () => {
vnode.state.editPermissionsIsOpen = true;
},
}),
isAdmin &&
proposal instanceof OffchainThread &&
m(TopicEditorMenuItem, {
openTopicEditor: () => {
vnode.state.topicEditorIsOpen = true;
},
}),
(isAuthor || isAdmin || app.user.isSiteAdmin) &&
m(ProposalBodyDeleteMenuItem, { item: proposal }),
(isAuthor || isAdmin) &&
m(ProposalHeaderPrivacyMenuItems, {
proposal,
getSetGlobalEditingStatus,
}),
(isAuthor || isAdmin) &&
app.chain?.meta.snapshot.length > 0 &&
m(MenuItem, {
onclick: () => {
const snapshotSpaces =
app.chain.meta.snapshot;
if (snapshotSpaces.length > 1) {
navigateToSubpage('/multiple-snapshots', {
action: 'create-from-thread',
proposal,
});
} else {
navigateToSubpage(
`/snapshot/${snapshotSpaces}`
);
}
},
label: 'Snapshot proposal from thread',
}),
// (isAuthor || isAdmin) &&
// m(ProposalHeaderLinkThreadsMenuItem, {
// item: proposal,
// }),
(isAuthor || isAdmin) && m(MenuDivider),
m(ThreadSubscriptionMenuItem, {
proposal: proposal as OffchainThread,
}),
],
inline: true,
trigger: m(CWIcon, {
iconName: 'chevronDown',
iconSize: 'small',
}),
}),
!app.isCustomDomain() &&
m('.CommentSocialHeader', [m(SocialSharingCarat)]),
vnode.state.editPermissionsIsOpen &&
proposal instanceof OffchainThread &&
m(ProposalEditorPermissions, {
thread: vnode.attrs.proposal as OffchainThread,
popoverMenu: true,
openStateHandler: (v) => {
vnode.state.editPermissionsIsOpen = v;
},
// TODO: Onchange logic
onChangeHandler: () => {},
}),
vnode.state.topicEditorIsOpen &&
proposal instanceof OffchainThread &&
m(TopicEditor, {
thread: vnode.attrs.proposal as OffchainThread,
popoverMenu: true,
onChangeHandler: (topic: OffchainTopic) => {
proposal.topic = topic;
m.redraw();
},
openStateHandler: (v) => {
vnode.state.topicEditorIsOpen = v;
m.redraw();
},
}),
vnode.attrs.stageEditorIsOpen &&
proposal instanceof OffchainThread &&
m(StageEditor, {
thread: vnode.attrs.proposal as OffchainThread,
popoverMenu: true,
onChangeHandler: (
stage: OffchainThreadStage,
chainEntities: ChainEntity[],
snapshotProposal: SnapshotProposal[]
) => {
proposal.stage = stage;
proposal.chainEntities = chainEntities;
if (app.chain?.meta.snapshot) {
proposal.snapshotProposal = snapshotProposal[0]?.id;
}
app.threads.fetchThreadsFromId([proposal.identifier]);
m.redraw();
},
openStateHandler: (v) => {
if (!v) vnode.attrs.closeStageEditor();
m.redraw();
},
}),
vnode.attrs.pollEditorIsOpen &&
proposal instanceof OffchainThread &&
m(PollEditor, {
thread: vnode.attrs.proposal as OffchainThread,
onChangeHandler: () => {
vnode.attrs.closePollEditor();
m.redraw();
},
}),
]
: [
m(ProposalBodyAuthor, { item: proposal }),
m(ProposalHeaderOnchainStatus, { proposal }),
app.isLoggedIn() &&
(isAdmin || isAuthor) &&
!getSetGlobalEditingStatus(GlobalStatus.Get) &&
proposalTitleIsEditable &&
m(PopoverMenu, {
transitionDuration: 0,
closeOnOutsideClick: true,
closeOnContentClick: true,
menuAttrs: { size: 'default' },
content: [
m(ProposalTitleEditMenuItem, {
item: proposal,
proposalPageState,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
],
inline: true,
trigger: m(CWIcon, {
iconName: 'chevronDown',
iconSize: 'small',
}),
}),
]
),
m('.proposal-body-link', [
proposal instanceof OffchainThread &&
proposal.kind === OffchainThreadKind.Link && [
vnode.state.editing
? m(ProposalLinkEditor, {
item: proposal,
parentState: vnode.state,
})
: m(ProposalHeaderExternalLink, { proposal }),
],
!(proposal instanceof OffchainThread) &&
(proposal['blockExplorerLink'] ||
proposal['votingInterfaceLink'] ||
proposal.threadId) &&
m('.proposal-body-link', [
proposal.threadId &&
m(ProposalHeaderThreadLink, { proposal }),
proposal['blockExplorerLink'] &&
m(ProposalHeaderBlockExplorerLink, { proposal }),
proposal['votingInterfaceLink'] &&
m(ProposalHeaderVotingInterfaceLink, { proposal }),
]),
]),
]),
]),
proposal instanceof OffchainThread &&
m('.proposal-content', [
(commentCount > 0 || app.user.activeAccount) &&
m('.thread-connector'),
m('.proposal-content-left', [
m(ProposalBodyAvatar, { item: proposal }),
]),
m('.proposal-content-right', [
!vnode.state.editing && m(ProposalBodyText, { item: proposal }),
!vnode.state.editing &&
attachments &&
attachments.length > 0 &&
m(ProposalBodyAttachments, { item: proposal }),
vnode.state.editing &&
m(ProposalBodyEditor, {
item: proposal,
parentState: vnode.state,
}),
m('.proposal-body-bottom', [
vnode.state.editing &&
m('.proposal-body-button-group', [
m(ProposalBodySaveEdit, {
item: proposal,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
m(ProposalBodyCancelEdit, {
item: proposal,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
]),
!vnode.state.editing &&
m('.proposal-response-row', [
m(ThreadReactionButton, {
thread: proposal,
}),
m(InlineReplyButton, {
commentReplyCount: commentCount,
onclick: () => {
if (!proposalPageState.replying) {
proposalPageState.replying = true;
scrollToForm();
} else if (!proposalPageState.parentCommentId) {
// If user is already replying to top-level, cancel reply
proposalPageState.replying = false;
}
proposalPageState.parentCommentId = null;
},
}),
]),
]),
]),
]),
!(proposal instanceof OffchainThread) &&
hasBody &&
m('.proposal-content', [m(ProposalBodyText, { item: proposal })]),
]
);
},
}
Example #10
Source File: index.ts From commonwealth with GNU General Public License v3.0 | 4 votes |
ProposalComment: m.Component<
{
comment: OffchainComment<any>;
getSetGlobalEditingStatus: CallableFunction;
proposalPageState: IProposalPageState;
parent: AnyProposal | OffchainComment<any> | OffchainThread;
proposal: AnyProposal | OffchainThread;
callback?: Function;
isAdmin?: boolean;
isLast: boolean;
},
{
editing: boolean;
saving: boolean;
replying: boolean;
quillEditorState: any;
}
> = {
view: (vnode) => {
const {
comment,
getSetGlobalEditingStatus,
proposalPageState,
proposal,
callback,
isAdmin,
isLast,
} = vnode.attrs;
if (!comment) return;
const parentType = comment.parentComment
? CommentParent.Comment
: CommentParent.Proposal;
const commentLink = getProposalUrlPath(
proposal.slug,
`${proposal.identifier}-${slugify(proposal.title)}?comment=${comment.id}`
);
const commentReplyCount = app.comments
.getByProposal(proposal)
.filter((c) => c.parentComment === comment.id && !c.deleted).length;
return m(
'.ProposalComment',
{
class: `${parentType}-child comment-${comment.id}`,
onchange: () => m.redraw(), // TODO: avoid catching bubbled input events
},
[
(!isLast || app.user.activeAccount) && m('.thread-connector'),
m('.comment-avatar', [m(ProposalBodyAvatar, { item: comment })]),
m('.comment-body', [
m('.comment-body-top', [
m(ProposalBodyAuthor, { item: comment }),
m(ProposalBodyCreated, { item: comment, link: commentLink }),
m(ProposalBodyLastEdited, { item: comment }),
((!vnode.state.editing &&
app.user.activeAccount &&
!getSetGlobalEditingStatus(GlobalStatus.Get) &&
app.user.activeAccount?.chain.id === comment.authorChain &&
app.user.activeAccount?.address === comment.author) ||
isAdmin) && [
m(PopoverMenu, {
closeOnContentClick: true,
content: [
app.user.activeAccount?.address === comment.author &&
m(ProposalBodyEditMenuItem, {
item: comment,
proposalPageState,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
m(ProposalBodyDeleteMenuItem, {
item: comment,
refresh: () => callback(),
}),
],
transitionDuration: 0,
trigger: m(CWIcon, {
iconName: 'chevronDown',
iconSize: 'small',
}),
}),
],
!app.isCustomDomain() &&
m('.CommentSocialHeader', [
m(SocialSharingCarat, { commentID: comment.id }),
]),
// For now, we are limiting threading to 1 level deep
// Comments whose parents are other comments should not display the reply option
// !vnode.state.editing
// && app.user.activeAccount
// && !getSetGlobalEditingStatus(GlobalStatus.Get)
// && parentType === CommentParent.Proposal
// && [
// m(ProposalBodyReply, {
// item: comment,
// getSetGlobalReplyStatus,
// parentType,
// parentState: vnode.state,
// }),
// ],
]),
m('.comment-body-content', [
!vnode.state.editing && m(ProposalBodyText, { item: comment }),
!vnode.state.editing &&
comment.attachments &&
comment.attachments.length > 0 &&
m(ProposalBodyAttachments, { item: comment }),
vnode.state.editing &&
m(ProposalBodyEditor, {
item: comment,
parentState: vnode.state,
}),
]),
m('.comment-body-bottom', [
vnode.state.editing &&
m('.comment-edit-buttons', [
m(ProposalBodySaveEdit, {
item: comment,
getSetGlobalEditingStatus,
parentState: vnode.state,
callback,
}),
m(ProposalBodyCancelEdit, {
item: comment,
getSetGlobalEditingStatus,
parentState: vnode.state,
}),
]),
!vnode.state.editing &&
!comment.deleted &&
m('.comment-response-row', [
m(CommentReactionButton, {
comment,
}),
m(InlineReplyButton, {
commentReplyCount,
onclick: () => {
if (
!proposalPageState.replying ||
proposalPageState.parentCommentId !== comment.id
) {
proposalPageState.replying = true;
proposalPageState.parentCommentId = comment.id;
scrollToForm(comment.id);
} else {
proposalPageState.replying = false;
}
},
}),
]),
]),
]),
]
);
},
}
Example #11
Source File: index.ts From commonwealth with GNU General Public License v3.0 | 4 votes |
ViewProposalPage: m.Component<
{
identifier: string;
type?: string;
},
IProposalPageState
> = {
oncreate: (vnode) => {
// writes type field if accessed as /proposal/XXX (shortcut for non-substrate chains)
if (!vnode.state.editing) {
vnode.state.editing = false;
}
},
view: (vnode) => {
const { identifier } = vnode.attrs;
const isDiscussion = pathIsDiscussion(app.activeChainId(), m.route.get());
if (!app.chain?.meta && !isDiscussion) {
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: 'Loading...',
});
}
const type =
vnode.attrs.type ||
(isDiscussion
? ProposalType.OffchainThread
: chainToProposalSlug(app.chain.meta));
const headerTitle = isDiscussion ? 'Discussions' : 'Proposals';
if (typeof identifier !== 'string')
return m(PageNotFound, { title: headerTitle });
const proposalId = identifier.split('-')[0];
const proposalType = type;
const proposalIdAndType = `${proposalId}-${proposalType}`;
// we will want to prefetch comments, profiles, and viewCount on the page before rendering anything
if (!vnode.state.prefetch || !vnode.state.prefetch[proposalIdAndType]) {
vnode.state.prefetch = {};
vnode.state.prefetch[proposalIdAndType] = {
commentsStarted: false,
pollsStarted: false,
viewCountStarted: false,
profilesStarted: false,
profilesFinished: false,
};
}
if (vnode.state.threadFetchFailed) {
return m(PageNotFound, { title: headerTitle });
}
// load app controller
if (!app.threads.initialized) {
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
}
const proposalRecentlyEdited = vnode.state.recentlyEdited;
const proposalDoesNotMatch =
vnode.state.proposal &&
(+vnode.state.proposal.identifier !== +proposalId ||
vnode.state.proposal.slug !== proposalType);
if (proposalDoesNotMatch) {
vnode.state.proposal = undefined;
vnode.state.recentlyEdited = false;
vnode.state.threadFetched = false;
}
// load proposal, and return m(PageLoading)
if (!vnode.state.proposal || proposalRecentlyEdited) {
try {
vnode.state.proposal = idToProposal(proposalType, proposalId);
} catch (e) {
// proposal might be loading, if it's not an offchain thread
if (proposalType === ProposalType.OffchainThread) {
if (!vnode.state.threadFetched) {
app.threads
.fetchThreadsFromId([+proposalId])
.then((res) => {
vnode.state.proposal = res[0];
m.redraw();
})
.catch((err) => {
notifyError('Thread not found');
vnode.state.threadFetchFailed = true;
});
vnode.state.threadFetched = true;
}
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
} else {
if (!app.chain.loaded) {
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
}
// check if module is still initializing
const c = proposalSlugToClass().get(proposalType) as ProposalModule<
any,
any,
any
>;
if (!c) {
return m(PageNotFound, { message: 'Invalid proposal type' });
}
if (!c.ready) {
// TODO: perhaps we should be able to load here without fetching ALL proposal data
// load sibling modules too
if (app.chain.base === ChainBase.Substrate) {
const chain = app.chain as Substrate;
app.chain.loadModules([
chain.council,
chain.technicalCommittee,
chain.treasury,
chain.democracyProposals,
chain.democracy,
chain.tips,
]);
} else {
app.chain.loadModules([c]);
}
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
}
}
// proposal does not exist, 404
return m(PageNotFound, { message: 'Proposal not found' });
}
}
const { proposal } = vnode.state;
if (proposalRecentlyEdited) vnode.state.recentlyEdited = false;
if (identifier !== `${proposalId}-${slugify(proposal.title)}`) {
navigateToSubpage(
getProposalUrlPath(
proposal.slug,
`${proposalId}-${slugify(proposal.title)}`,
true
),
{},
{ replace: true }
);
}
// load proposal
if (!vnode.state.prefetch[proposalIdAndType]['threadReactionsStarted']) {
app.threads.fetchReactionsCount([proposal]).then(() => m.redraw);
vnode.state.prefetch[proposalIdAndType]['threadReactionsStarted'] = true;
}
// load comments
if (!vnode.state.prefetch[proposalIdAndType]['commentsStarted']) {
app.comments
.refresh(proposal, app.activeChainId())
.then(async () => {
vnode.state.comments = app.comments
.getByProposal(proposal)
.filter((c) => c.parentComment === null);
// fetch reactions
const { result: reactionCounts } = await $.ajax({
type: 'POST',
url: `${app.serverUrl()}/reactionsCounts`,
headers: {
'content-type': 'application/json',
},
data: JSON.stringify({
proposal_ids: [proposalId],
comment_ids: app.comments
.getByProposal(proposal)
.map((comment) => comment.id),
active_address: app.user.activeAccount?.address,
}),
});
// app.reactionCounts.deinit()
for (const rc of reactionCounts) {
const id = app.reactionCounts.store.getIdentifier({
threadId: rc.thread_id,
proposalId: rc.proposal_id,
commentId: rc.comment_id,
});
app.reactionCounts.store.add(
modelReactionCountFromServer({ ...rc, id })
);
}
m.redraw();
})
.catch(() => {
notifyError('Failed to load comments');
vnode.state.comments = [];
m.redraw();
});
vnode.state.prefetch[proposalIdAndType]['commentsStarted'] = true;
}
if (vnode.state.comments?.length) {
const mismatchedComments = vnode.state.comments.filter((c) => {
return c.rootProposal !== `${type}_${proposalId}`;
});
if (mismatchedComments.length) {
vnode.state.prefetch[proposalIdAndType]['commentsStarted'] = false;
}
}
const createdCommentCallback = () => {
vnode.state.comments = app.comments
.getByProposal(proposal)
.filter((c) => c.parentComment === null);
m.redraw();
};
// load polls
if (
proposal instanceof OffchainThread &&
!vnode.state.prefetch[proposalIdAndType]['pollsStarted']
) {
app.polls.fetchPolls(app.activeChainId(), proposal.id).catch(() => {
notifyError('Failed to load comments');
vnode.state.comments = [];
m.redraw();
});
vnode.state.prefetch[proposalIdAndType]['pollsStarted'] = true;
} else if (proposal instanceof OffchainThread) {
vnode.state.polls = app.polls.getByThreadId(proposal.id);
}
// load view count
if (
!vnode.state.prefetch[proposalIdAndType]['viewCountStarted'] &&
proposal instanceof OffchainThread
) {
$.post(`${app.serverUrl()}/viewCount`, {
chain: app.activeChainId(),
object_id: proposal.id, // (proposal instanceof OffchainThread) ? proposal.id : proposal.slug,
})
.then((response) => {
if (response.status !== 'Success') {
vnode.state.viewCount = 0;
throw new Error(`got unsuccessful status: ${response.status}`);
} else {
vnode.state.viewCount = response.result.view_count;
m.redraw();
}
})
.catch(() => {
vnode.state.viewCount = 0;
throw new Error('could not load view count');
});
vnode.state.prefetch[proposalIdAndType]['viewCountStarted'] = true;
} else if (!vnode.state.prefetch[proposalIdAndType]['viewCountStarted']) {
// view counts currently not supported for proposals
vnode.state.prefetch[proposalIdAndType]['viewCountStarted'] = true;
vnode.state.viewCount = 0;
}
if (vnode.state.comments === undefined) {
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
}
if (vnode.state.viewCount === undefined) {
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
}
// load profiles
if (
vnode.state.prefetch[proposalIdAndType]['profilesStarted'] === undefined
) {
if (proposal instanceof OffchainThread) {
app.profiles.getProfile(proposal.authorChain, proposal.author);
} else if (proposal.author instanceof Account) {
// AnyProposal
app.profiles.getProfile(
proposal.author.chain.id,
proposal.author.address
);
}
vnode.state.comments.forEach((comment) => {
app.profiles.getProfile(comment.authorChain, comment.author);
});
vnode.state.prefetch[proposalIdAndType]['profilesStarted'] = true;
}
if (
!app.profiles.allLoaded() &&
!vnode.state.prefetch[proposalIdAndType]['profilesFinished']
) {
return m(PageLoading, {
narrow: true,
showNewProposalButton: true,
title: headerTitle,
});
}
vnode.state.prefetch[proposalIdAndType]['profilesFinished'] = true;
const windowListener = (e) => {
if (vnode.state.editing || activeQuillEditorHasText()) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', windowListener);
const comments = vnode.state.comments;
const viewCount: number = vnode.state.viewCount;
const commentCount: number = app.comments.nComments(proposal);
const voterCount: number =
proposal instanceof OffchainThread ? 0 : proposal.getVotes().length;
const getSetGlobalEditingStatus = (call: string, status?: boolean) => {
if (call === GlobalStatus.Get) return vnode.state.editing;
if (call === GlobalStatus.Set && status !== undefined) {
vnode.state.editing = status;
if (status === false) {
vnode.state.recentlyEdited = true;
}
m.redraw();
}
};
// Original posters have full editorial control, while added collaborators
// merely have access to the body and title
const { activeAccount } = app.user;
const authorChain =
proposal instanceof OffchainThread
? proposal.authorChain
: app.activeChainId();
const authorAddress =
proposal instanceof OffchainThread
? proposal.author
: proposal.author?.address;
const isAuthor =
activeAccount?.address === authorAddress &&
activeAccount?.chain.id === authorChain;
const isEditor =
(proposal as OffchainThread).collaborators?.filter((c) => {
return (
c.address === activeAccount?.address &&
c.chain === activeAccount?.chain.id
);
}).length > 0;
const isAdminOrMod =
app.user.isRoleOfCommunity({
role: 'admin',
chain: app.activeChainId(),
}) ||
app.user.isRoleOfCommunity({
role: 'moderator',
chain: app.activeChainId(),
});
const isAdmin = app.user.isRoleOfCommunity({
role: 'admin',
chain: app.activeChainId(),
});
if (proposal instanceof SubstrateTreasuryTip) {
const {
author,
title,
data: { who, reason },
} = proposal;
const contributors = proposal.getVotes();
return m(
Sublayout,
{
showNewProposalButton: true,
title: headerTitle,
},
[
m('.TipDetailPage', [
m('.tip-details', [
m('.title', title),
m('.proposal-page-row', [
m('.label', 'Finder'),
m(User, {
user: author,
linkify: true,
popover: true,
showAddressWithDisplayName: true,
}),
]),
m('.proposal-page-row', [
m('.label', 'Beneficiary'),
m(User, {
user: app.profiles.getProfile(proposal.author.chain.id, who),
linkify: true,
popover: true,
showAddressWithDisplayName: true,
}),
]),
m('.proposal-page-row', [
m('.label', 'Reason'),
m('.tip-reason', [m(MarkdownFormattedText, { doc: reason })]),
]),
m('.proposal-page-row', [
m('.label', 'Amount'),
m('.amount', [
m('.denominator', proposal.support.denom),
m('', proposal.support.inDollars),
]),
]),
]),
m('.tip-contributions', [
proposal.canVoteFrom(
app.user.activeAccount as SubstrateAccount
) &&
m('.contribute', [
m('.title', 'Contribute'),
m('.mb-12', [
m('.label', 'Amount'),
m(Input, {
name: 'amount',
placeholder: 'Enter tip amount',
autocomplete: 'off',
fluid: true,
oninput: (e) => {
const result = (e.target as any).value;
vnode.state.tipAmount =
result.length > 0
? app.chain.chain.coins(parseFloat(result), true)
: undefined;
m.redraw();
},
}),
]),
m(Button, {
disabled: vnode.state.tipAmount === undefined,
intent: 'primary',
rounded: true,
label: 'Submit Transaction',
onclick: (e) => {
e.preventDefault();
createTXModal(
proposal.submitVoteTx(
new DepositVote(
app.user.activeAccount,
vnode.state.tipAmount
)
)
);
},
tabindex: 4,
type: 'submit',
}),
]),
contributors.length > 0 && [
m('.contributors .title', 'Contributors'),
contributors.map(({ account, deposit }) =>
m('.contributors-row', [
m('.amount', [
m('.denominator', deposit.denom),
m('', deposit.inDollars),
]),
m(User, {
user: account,
linkify: true,
popover: true,
showAddressWithDisplayName: true,
}),
])
),
],
]),
]),
]
);
}
const showLinkedSnapshotOptions =
(proposal as OffchainThread).snapshotProposal?.length > 0 ||
(proposal as OffchainThread).chainEntities?.length > 0 ||
isAuthor ||
isAdminOrMod;
const showLinkedThreadOptions =
(proposal as OffchainThread).linkedThreads?.length > 0 ||
isAuthor ||
isAdminOrMod;
return m(
Sublayout,
{
showNewProposalButton: true,
title: headerTitle,
},
m('.ViewProposalPage', [
m('.view-proposal-page-container', [
[
m(ProposalHeader, {
proposal,
commentCount,
viewCount,
getSetGlobalEditingStatus,
proposalPageState: vnode.state,
isAuthor,
isEditor,
isAdmin: isAdminOrMod,
stageEditorIsOpen: vnode.state.stageEditorIsOpen,
pollEditorIsOpen: vnode.state.pollEditorIsOpen,
closeStageEditor: () => {
vnode.state.stageEditorIsOpen = false;
m.redraw();
},
closePollEditor: () => {
vnode.state.pollEditorIsOpen = false;
m.redraw();
},
}),
!(proposal instanceof OffchainThread) &&
m(LinkedProposalsEmbed, { proposal }),
proposal instanceof AaveProposal && [
m(AaveViewProposalSummary, { proposal }),
m(AaveViewProposalDetail, { proposal }),
],
!(proposal instanceof OffchainThread) &&
m(ProposalVotingResults, { proposal }),
!(proposal instanceof OffchainThread) &&
m(ProposalVotingActions, { proposal }),
m(ProposalComments, {
proposal,
comments,
createdCommentCallback,
getSetGlobalEditingStatus,
proposalPageState: vnode.state,
recentlySubmitted: vnode.state.recentlySubmitted,
isAdmin: isAdminOrMod,
}),
!vnode.state.editing &&
!vnode.state.parentCommentId &&
m(CreateComment, {
callback: createdCommentCallback,
cancellable: true,
getSetGlobalEditingStatus,
proposalPageState: vnode.state,
parentComment: null,
rootProposal: proposal,
}),
],
]),
m('.right-content-container', [
[
showLinkedSnapshotOptions &&
proposal instanceof OffchainThread &&
m(LinkedProposalsCard, {
proposal,
openStageEditor: () => {
vnode.state.stageEditorIsOpen = true;
},
showAddProposalButton: isAuthor || isAdminOrMod,
}),
showLinkedThreadOptions &&
proposal instanceof OffchainThread &&
m(LinkedThreadsCard, {
proposalId: proposal.id,
allowLinking: isAuthor || isAdminOrMod,
}),
proposal instanceof OffchainThread &&
isAuthor &&
(!app.chain?.meta?.adminOnlyPolling || isAdmin) &&
m(PollEditorCard, {
proposal,
proposalAlreadyHasPolling: !vnode.state.polls?.length,
openPollEditor: () => {
vnode.state.pollEditorIsOpen = true;
},
}),
proposal instanceof OffchainThread &&
[
...new Map(
vnode.state.polls?.map((poll) => [poll.id, poll])
).values(),
].map((poll) => {
return m(ProposalPoll, { poll, thread: proposal });
}),
],
]),
])
);
},
}