diff --git a/apps/discussions/app/ipfs/index.js b/apps/discussions/app/ipfs/index.js index a7500eb36..4d8710222 100644 --- a/apps/discussions/app/ipfs/index.js +++ b/apps/discussions/app/ipfs/index.js @@ -1,7 +1,22 @@ +import axios from 'axios' import ipfsClient from 'ipfs-http-client' -export const ipfs = ipfsClient({ - host: 'ipfs.autark.xyz', - port: '5001', - protocol: 'https', -}) +const environments = { + development: { host: 'localhost', port: '5001', protocol: 'http' }, + production: { host: 'ipfs.autark.xyz', port: '5001', protocol: 'https' }, + staging: { host: 'ipfs.autark.xyz', port: '5001', protocol: 'https' }, +} + +const config = environments[process.env.NODE_ENV] + +export const ipfs = ipfsClient(config) + +export const ipfsGet = async hash => { + const endpoint = `${config.protocol}://${config.host}:8080/ipfs/${hash}` + try { + const { data } = await axios.get(endpoint) + return data + } catch (err) { + console.error('Error getting data from IPFS', err) + } +} diff --git a/apps/discussions/app/script.js b/apps/discussions/app/script.js index 45bd92f0d..13a07ac77 100644 --- a/apps/discussions/app/script.js +++ b/apps/discussions/app/script.js @@ -2,7 +2,14 @@ import '@babel/polyfill' import { of } from 'rxjs' import AragonApi from '@aragon/api' -import { handleHide, handlePost, handleRevise, initialState } from './state' +import { + updateThread, + deleteThread, + handleHide, + handlePost, + handleRevise, + initialState +} from './state' const INITIALIZATION_TRIGGER = Symbol('INITIALIZATION_TRIGGER') @@ -13,16 +20,22 @@ api.store( let newState switch (event.event) { case INITIALIZATION_TRIGGER: - newState = { ...initialState, syncing: false } + newState = { ...initialState, isSyncing: false } return newState case 'SYNC_STATUS_SYNCING': - newState = { ...initialState, syncing: true } + newState = { ...initialState, isSyncing: true } return newState case 'SYNC_STATUS_SYNCED': - newState = { ...initialState, syncing: false } + newState = { ...initialState, isSyncing: false } return newState case 'ACCOUNTS_TRIGGER': - newState = { ...initialState, syncing: false } + newState = { ...initialState, isSyncing: false } + return newState + case 'UpdateThread': + newState = await updateThread(state, event.returnValues) + return newState + case 'DeleteThread': + newState = await deleteThread(state, event.returnValues) return newState case 'Post': newState = await handlePost(state, event) diff --git a/apps/discussions/app/state/index.js b/apps/discussions/app/state/index.js index 47f38a278..d3cc80b84 100644 --- a/apps/discussions/app/state/index.js +++ b/apps/discussions/app/state/index.js @@ -1,2 +1,3 @@ export * from './initialState' export * from './updateState' +export * from './threads' diff --git a/apps/discussions/app/state/initialState.js b/apps/discussions/app/state/initialState.js index 84a0ec732..0d84cdfda 100644 --- a/apps/discussions/app/state/initialState.js +++ b/apps/discussions/app/state/initialState.js @@ -1,4 +1,5 @@ export const initialState = { + threads: [], discussions: {}, - syncing: true, + isSyncing: true, } diff --git a/apps/discussions/app/state/threads.js b/apps/discussions/app/state/threads.js new file mode 100644 index 000000000..d419cc89e --- /dev/null +++ b/apps/discussions/app/state/threads.js @@ -0,0 +1,44 @@ +import { ipfsGet } from '../ipfs' + +export const updateThread = async (state, { thread, metadata }) => { + const { threads = [] } = state + try { + const { + name, + title, + description, + creationDate, + context, + author, + } = await ipfsGet(metadata) + const index = threads.findIndex(t => t.address === thread) + if (index > -1) threads[index].description = description + else { + threads.push({ + address: thread, + name, + title, + description, + context, + creationDate: new Date(creationDate), + author, + }) + } + return { + ...state, + threads + } + } catch (e) { + console.error(e) + return state + } +} + +export const deleteThread = async (state, { thread }) => { + const { threads = [] } = state + const filteredThreads = threads.filter(t => t.address !== thread) + return { + ...state, + threads: filteredThreads + } +} diff --git a/apps/discussions/arapp.json b/apps/discussions/arapp.json index e9e983577..a04e29e70 100644 --- a/apps/discussions/arapp.json +++ b/apps/discussions/arapp.json @@ -1,8 +1,13 @@ { "roles": [ { - "name": "Is doing nothing, just needed to technically be an AragonApp", - "id": "EMPTY_ROLE", + "name": "Register role", + "id": "REGISTER_ROLE", + "params": [] + }, + { + "name": "Moderator role", + "id": "MODERATOR_ROLE", "params": [] } ], diff --git a/apps/discussions/contracts/DiscussionApp.sol b/apps/discussions/contracts/DiscussionApp.sol index 7b69881b2..fdf4bc1cf 100644 --- a/apps/discussions/contracts/DiscussionApp.sol +++ b/apps/discussions/contracts/DiscussionApp.sol @@ -19,10 +19,16 @@ contract DiscussionApp is IForwarder, AragonApp { ); event Hide(address indexed author, uint256 discussionThreadId, uint256 postId, uint256 hiddenAt); event CreateDiscussionThread(uint256 actionId, bytes _evmScript); + event UpdateThread(string thread, string metadata); + event DeleteThread(string thread); - bytes32 public constant EMPTY_ROLE = keccak256("EMPTY_ROLE"); + bytes32 public constant REGISTER_ROLE = keccak256("REGISTER_ROLE"); + bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE"); string private constant ERROR_CAN_NOT_FORWARD = "DISCUSSIONS_CAN_NOT_FORWARD"; + string private constant ERROR_ALREADY_EXISTS = "You cannot create an existing thread."; + string private constant ERROR_EDIT_NOT_AUTHOR = "You cannot edit a thread you did not author."; + string private constant ERROR_DELETE_NOT_AUTHOR = "You cannot delete a thread you did not author."; struct DiscussionPost { address author; @@ -38,6 +44,8 @@ contract DiscussionApp is IForwarder, AragonApp { mapping(uint256 => DiscussionPost[]) public discussionThreadPosts; + mapping(string => address) private threadAuthors; + function initialize() external onlyInit { discussionThreadId = 0; initialized(); @@ -96,6 +104,38 @@ contract DiscussionApp is IForwarder, AragonApp { ); } + /** + * @notice Create new thread at `thread` + * @param thread The address of the 3Box thread + * @param metadata The IPFS hash of the metadata for the thread + */ + function registerThread(string thread, string metadata) + auth(REGISTER_ROLE) external { + require(threadAuthors[thread] == 0, ERROR_ALREADY_EXISTS); + threadAuthors[thread] = msg.sender; + emit UpdateThread(thread, metadata); + } + + /** + * @notice Edit thread at `thread` + * @param thread The address of the 3Box thread + * @param newMetadata The IPFS hash of the new metadata for the thread + */ + function editThread(string thread, string newMetadata) + auth(REGISTER_ROLE) external { + require(threadAuthors[thread] == msg.sender, ERROR_EDIT_NOT_AUTHOR); + emit UpdateThread(thread, newMetadata); + } + + /** + * @notice Delete thread at `thread` + * @param thread The address of the 3Box thread + */ + function deleteThread(string thread) auth(REGISTER_ROLE) external { + require(threadAuthors[thread] == msg.sender, ERROR_DELETE_NOT_AUTHOR); + emit DeleteThread(thread); + } + // Forwarding fns /** diff --git a/apps/discussions/package.json b/apps/discussions/package.json index 6b53ae23f..2be9cb4ad 100644 --- a/apps/discussions/package.json +++ b/apps/discussions/package.json @@ -46,7 +46,7 @@ "clean:start": "aragon clean && rm -rf ~/.aragon && npm run start:http:template", "start": "npm run start:ipfs", "start:ipfs": "aragon run", - "start:http": "aragon run --http localhost:8001 --http-served-from ./dist", + "start:http": "NODE_ENV=development aragon run --http localhost:8001 --http-served-from ./dist", "start:ipfs:template": "npm run start:ipfs -- --template Template --template-init @ARAGON_ENS", "start:http:template": "rm -rf build && npm run start:http -- --template Template --template-init @ARAGON_ENS", "start:app": "cd app && npm start && cd ..", diff --git a/apps/discussions/public/meta/icon.svg b/apps/discussions/public/meta/icon.svg index 546d85afe..394a0917f 100644 --- a/apps/discussions/public/meta/icon.svg +++ b/apps/discussions/public/meta/icon.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/package.json b/package.json index f639af030..9689d78ed 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lerna": "^3.10.8", "truffle-privatekey-provider": "1.1.0", "wait-on": "^3.2.0", - "web3": "^1.2.4" + "web3": "1.2.7" }, "scripts": { "bootstrap": "lerna bootstrap --hoist", @@ -74,7 +74,7 @@ "start:dev-template": "cd templates/dev && npm run publish:aragen && npm run start:template:aragen", "start:address": "cd apps/address-book && npm start", "start:allocations": "cd apps/allocations && npm start", - "start:dev": "DEV=true apps/shared/test-helpers/ganache-cli.sh", + "start:dev": "NODE_ENV=development DEV=true apps/shared/test-helpers/ganache-cli.sh", "start:no:client": "NO_CLIENT=true apps/shared/test-helpers/ganache-cli.sh", "start:dot": "cd apps/dot-voting && npm start", "start:projects": "cd apps/projects && npm start", diff --git a/templates/dev/contracts/BaseCache.sol b/templates/dev/contracts/BaseCache.sol index 67d5ec392..d0b32907a 100644 --- a/templates/dev/contracts/BaseCache.sol +++ b/templates/dev/contracts/BaseCache.sol @@ -23,7 +23,7 @@ contract BaseCache is BaseTemplate { ENS(_deployedSetupContracts[1]), MiniMeTokenFactory(_deployedSetupContracts[2]), IFIFSResolvingRegistrar(_deployedSetupContracts[3]) - ) {} + ) public {} function _cacheBase( ACL _acl, diff --git a/templates/dev/contracts/BaseOEApps.sol b/templates/dev/contracts/BaseOEApps.sol index b7638127d..98e74983b 100644 --- a/templates/dev/contracts/BaseOEApps.sol +++ b/templates/dev/contracts/BaseOEApps.sol @@ -123,11 +123,13 @@ contract BaseOEApps is BaseCache, TokenCache { /* DISCUSSIONS */ function _installDiscussionsApp(Kernel _dao) internal returns (DiscussionApp) { - return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID)); + bytes memory initializeData = abi.encodeWithSelector(DiscussionApp(0).initialize.selector); + return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID, initializeData)); } - function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _grantee, address _manager) internal { - _acl.createPermission(_grantee, _discussions, _discussions.EMPTY_ROLE(), _manager); + function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _moderator, address _register, address _manager) internal { + _acl.createPermission(_moderator, _discussions, _discussions.MODERATOR_ROLE(), _manager); + _acl.createPermission(_register, _discussions, _discussions.REGISTER_ROLE(), _manager); } /* PROJECTS */ diff --git a/templates/dev/contracts/DevTemplate.sol b/templates/dev/contracts/DevTemplate.sol index ebb0f9afd..351cf33e1 100644 --- a/templates/dev/contracts/DevTemplate.sol +++ b/templates/dev/contracts/DevTemplate.sol @@ -158,11 +158,6 @@ contract DevTemplate is BaseOEApps { ) internal { - if (_useDiscussions) { - DiscussionApp discussions = _installDiscussionsApp(_dao); - _createDiscussionsPermissions(_acl, discussions, ANY_ENTITY, _voting); - } - MiniMeToken token = _popTokenCache(msg.sender); AddressBook addressBook = _installAddressBookApp(_dao); Allocations allocations = _installAllocationsApp(_dao, _vault, _allocationsPeriod == 0 ? DEFAULT_PERIOD : _allocationsPeriod); @@ -170,6 +165,14 @@ contract DevTemplate is BaseOEApps { Projects projects = _installProjectsApp(_dao, _vault); Rewards rewards = _installRewardsApp(_dao, _vault); + if (_useDiscussions) { + DiscussionApp discussions = _installDiscussionsApp(_dao); + // _createDiscussionsPermissions(_acl, discussions, msg.sender, _tokenManager, _voting); // Stack too deep + _acl.createPermission(msg.sender, discussions, discussions.MODERATOR_ROLE(), _voting); + _acl.createPermission(_tokenManager, discussions, discussions.REGISTER_ROLE(), _voting); + } + + _setupOEPermissions( _acl, _tokenManager,