diff --git a/contract/talk.cpp b/contract/talk.cpp index 63028339..55ecf022 100644 --- a/contract/talk.cpp +++ b/contract/talk.cpp @@ -1,5 +1,8 @@ #include +using react_t = uint8_t; +constexpr react_t REACT_TYPE_MAX = 5; + // Message table struct [[eosio::table("message"), eosio::contract("talk")]] message { uint64_t id = {}; // Non-0 @@ -11,8 +14,36 @@ struct [[eosio::table("message"), eosio::contract("talk")]] message { uint64_t get_reply_to() const { return reply_to; } }; +uint128_t makeLikesUniqSecondary( uint64_t post_id, eosio::name user) { + return uint128_t{post_id} << 64 | user.value; +} + +// Likes table +struct [[eosio::table("likes"), eosio::contract("talk")]] likes { + uint64_t id = {}; // Non-0 + uint64_t post_id = {}; // Non-0 + eosio::name user = {}; + react_t react = {}; // reaction type + + uint64_t primary_key() const { return id; } + uint128_t get_uniq_secondary() const { return makeLikesUniqSecondary(post_id, user); } +}; + using message_table = eosio::multi_index< - "message"_n, message, eosio::indexed_by<"by.reply.to"_n, eosio::const_mem_fun>>; + "message"_n, message, + eosio::indexed_by< + "by.reply.to"_n, + eosio::const_mem_fun + > +>; + +using likes_table = eosio::multi_index< + "likes"_n, likes, + eosio::indexed_by< + "by.uniq.secondary"_n, + eosio::const_mem_fun + > +>; // The contract class talk : eosio::contract { @@ -28,9 +59,10 @@ class talk : eosio::contract { require_auth(user); // Check reply_to exists - if (reply_to) + if (reply_to) { table.get(reply_to); - + } + // Create an ID if user didn't specify one eosio::check(id < 1'000'000'000ull, "user-specified id is too big"); if (!id) @@ -44,4 +76,58 @@ class talk : eosio::contract { message.content = content; }); } + + // Like or unlike a message, or change reaction + // to like a message, react must be > 0 and <+ REACT_MAX + // to unlike a message, react must be 0 and like must exist in likes_table + [[eosio::action]] void likepost(uint64_t post_id, eosio::name user, react_t react) { + // Check user authentication + require_auth(user); + + auto msg = message_table{get_self(), 0}; + auto likes = likes_table{get_self(), 0}; + + // check for valid reaction + eosio::check(react <= REACT_TYPE_MAX, "Bad value for reaction"); + + // post_id must be > 0 + eosio::check(post_id > 0, "Must have valid post_id-to"); + + // Check reply_to exists + auto it_msg = msg.find(post_id); + + // cannot like your own post + eosio::check(it_msg->user != user, "Cannot like yur own post"); + + // determien if user already liked this post + auto idx = likes.get_index<"by.uniq.secondary"_n>(); + auto it = idx.find(makeLikesUniqSecondary(post_id, user)); + bool likeExists = (it != idx.end()); + + // if reaction is 0, post must already exist (it will be deleted) + if(react == 0) + eosio::check(likeExists, "Like not found in table"); + + // like does not exist, so insert new record + if (!likeExists) { + likes.emplace(get_self(), [&](auto& like) { + like.id = likes.available_primary_key(); + like.post_id = post_id; + like.user = user; + like.react = react; + }); + } + // othwerise like already exists in table, so either erase it, if reaction is 0 or update it it reaction is changed + else { + auto like_it = likes.find(it->id); + if (react == 0) { + likes.erase(like_it); + } else if (react != like_it->react) { + likes.modify(like_it, get_self(), [&](auto& like) { + like.react = react; + }); + } + } + + } }; diff --git a/talk_run.sh b/talk_run.sh new file mode 100755 index 00000000..6b63412e --- /dev/null +++ b/talk_run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +eosio-cpp contract/talk.cpp +cleos create account eosio talk EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV +cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV +cleos create account eosio jane EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV +cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV +cleos set code talk talk.wasm +cleos set abi talk talk.abi \ No newline at end of file diff --git a/tests/talk_tests.cpp b/tests/talk_tests.cpp index 5c6c1495..113b015a 100644 --- a/tests/talk_tests.cpp +++ b/tests/talk_tests.cpp @@ -23,6 +23,7 @@ BOOST_AUTO_TEST_CASE(post) try { // Create users t.create_account(N(john)); t.create_account(N(jane)); + t.create_account(N(jack)); // Test "post" action t.push_action( @@ -50,6 +51,43 @@ BOOST_AUTO_TEST_CASE(post) try { ("content", "post 3: reply") // ); + + // jane likes johns post + t.push_action( + N(talk), N(likepost), N(jane), + mutable_variant_object // + ("post_id", 1) // + ("user", "jane") // + ("react", 1) // + ); + + // jane changes reaction type from 'like' to 'love' + t.push_action( + N(talk), N(likepost), N(jane), + mutable_variant_object // + ("post_id", 1) // + ("user", "jane") // + ("react", 2) // + ); + + // jack also reacts to johns post + t.push_action( + N(talk), N(likepost), N(jack), + mutable_variant_object // + ("post_id", 1) // + ("user", "jack") // + ("react", 4) // + ); + + // jane deletes like + t.push_action( + N(talk), N(likepost), N(jane), + mutable_variant_object // + ("post_id", 1) // + ("user", "jane") // + ("react", 0) // + ); + // Can't reply to non-existing message BOOST_CHECK_THROW( [&] { @@ -63,6 +101,58 @@ BOOST_AUTO_TEST_CASE(post) try { ); }(), fc::exception); + + BOOST_CHECK_THROW( + [&] { + t.push_action( + N(talk), N(post), N(john), + mutable_variant_object // + ("id", 4) // + ("reply_to", 99) // + ("user", "john") // + ("content", "post 3: reply") // + ); + }(), + fc::exception); + + // must have proper authorization + BOOST_CHECK_THROW( + [&] { + t.push_action( + N(talk), N(likepost), N(john), + mutable_variant_object // + ("post_id", 1) // + ("user", "jane") // + ("react", 1) // + ); + }(), + fc::exception); + + // cannot like yoru own post + BOOST_CHECK_THROW( + [&] { + t.push_action( + N(talk), N(likepost), N(john), + mutable_variant_object // + ("post_id", 1) // + ("user", "john") // + ("react", 1) // + ); + }(), + fc::exception); + + // cannot like a post which does not exist + BOOST_CHECK_THROW( + [&] { + t.push_action( + N(talk), N(likepost), N(john), + mutable_variant_object // + ("post_id", 3) // + ("user", "john") // + ("react", 1) // + ); + }(), + fc::exception); } FC_LOG_AND_RETHROW() diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 0027d2a1..04bc2823 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -1,4 +1,4 @@ -// To see this in action, run this in a terminal: +/// To see this in action, run this in a terminal: // gp preview $(gp url 8000) import * as React from "react"; @@ -15,12 +15,24 @@ interface PostData { content?: string; }; +interface LikePostData { + id? :number; + user?: string; + post_id?: number; + react?: number; +}; + interface PostFormState { privateKey: string; data: PostData; + likeData: LikePostData; error: string; + likeError: string; }; +const reactStr = ["", "likes", "loves", "lol", "sad", "angry"]; + + class PostForm extends React.Component<{}, PostFormState> { api: Api; @@ -33,9 +45,16 @@ class PostForm extends React.Component<{}, PostFormState> { id: 0, user: 'bob', reply_to: 0, - content: 'This is a test' + content: 'This is a test', + }, + likeData: { + id: 0, + user: 'bob', + post_id: 0, + react: 0, }, error: '', + likeError: '', }; } @@ -43,6 +62,10 @@ class PostForm extends React.Component<{}, PostFormState> { this.setState({ data: { ...this.state.data, ...data } }); } + setLikeData(likeData: LikePostData) { + this.setState({ likeData: { ...this.state.likeData, ...likeData } }); + } + async post() { try { this.api.signatureProvider = new JsSignatureProvider([this.state.privateKey]); @@ -71,6 +94,34 @@ class PostForm extends React.Component<{}, PostFormState> { } } + async like() { + try { + this.api.signatureProvider = new JsSignatureProvider([this.state.privateKey]); + const result = await this.api.transact( + { + actions: [{ + account: 'talk', + name: 'likepost', + authorization: [{ + actor: this.state.likeData.user, + permission: 'active', + }], + data: this.state.likeData, + }] + }, { + blocksBehind: 3, + expireSeconds: 30, + }); + console.log(result); + this.setState({ likeError: '' }); + } catch (e) { + if (e.json) + this.setState({ likeError: JSON.stringify(e.json, null, 4) }); + else + this.setState({ likeError: '' + e }); + } + } + render() { return
@@ -116,6 +167,41 @@ class PostForm extends React.Component<{}, PostFormState> { Error:
{this.state.error}
} + +
+ + + + + + + + + + + + + + +
User this.setLikeData({ user: e.target.value })} + />
Post ID this.setLikeData({ post_id: +e.target.value })} + />
Reaction this.setLikeData({ react: +e.target.value })} + />
+ + {this.state.likeError &&
+
+ Error: +
{this.state.likeError}
+
}
; } } @@ -135,14 +221,57 @@ class Messages extends React.Component<{}, { content: string }> { json: true, code: 'talk', scope: '', table: 'message', limit: 1000, }); let content = - 'id reply_to user content\n' + - '=============================================================\n'; + 'id reply_to user content \n' + + '=======================================================================\n'; for (let row of rows.rows) content += (row.id + '').padEnd(12) + (row.reply_to + '').padEnd(12) + ' ' + row.user.padEnd(14) + - row.content + '\n'; + row.content.padEnd(24) + '\n'; + this.setState({ content }); + } catch (e) { + if (e.json) + this.setState({ content: JSON.stringify(e.json, null, 4) }); + else + this.setState({ content: '' + e }); + } + + }, 200); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + return
{this.state.content}
; + } +} + +class Likes extends React.Component<{}, { content: string }> { + interval: number; + + constructor(props: {}) { + super(props); + this.state = { content: '///' }; + } + + componentDidMount() { + this.interval = window.setInterval(async () => { + try { + let content = + 'id post_id user reaction \n' + + '===================================================================\n'; + const likeRows = await rpc.get_table_rows({ + json: true, code: 'talk', scope: '', table: 'likes', limit: 1000, + }); + for (let row of likeRows.rows) + content += + (row.id + '').padEnd(12) + + (row.post_id + '').padEnd(12) + ' ' + + row.user.padEnd(14) + + reactStr[row.react].padEnd(14) + '\n'; this.setState({ content }); } catch (e) { if (e.json) @@ -169,6 +298,8 @@ ReactDOM.render(
Messages: + Likes: + , document.getElementById("example") -); +); \ No newline at end of file