diff --git a/app/app/settings.py b/app/app/settings.py
index 56f8473a743..771d3f704b1 100644
--- a/app/app/settings.py
+++ b/app/app/settings.py
@@ -812,6 +812,7 @@ def callback(request):
MINICLR_ADDRESS = env('MINICLR_ADDRESS', default='0x00De4B13153673BCAE2616b67bf822500d325Fc3')
MINICLR_PRIVATE_KEY = env('MINICLR_PRIVATE_KEY', default='0x00De4B13153673BCAE2616b67bf822500d325Fc3')
+
AVATAR_ADDRESS = env('AVATAR_ADDRESS', default='0x00De4B13153673BCAE2616b67bf822500d325Fc3')
AVATAR_PRIVATE_KEY = env('AVATAR_PRIVATE_KEY', default='0x00De4B13153673BCAE2616b67bf822500d325Fc3')
diff --git a/app/assets/onepager/js/send.js b/app/assets/onepager/js/send.js
index babaf9857d4..5c0ded2ab8f 100644
--- a/app/assets/onepager/js/send.js
+++ b/app/assets/onepager/js/send.js
@@ -170,7 +170,7 @@ function isNumeric(n) {
}
-function sendTip(email, github_url, from_name, username, amount, comments_public, comments_priv, from_email, accept_tos, tokenAddress, expires, success_callback, failure_callback, is_for_bounty_fulfiller) {
+function sendTip(email, github_url, from_name, username, amount, comments_public, comments_priv, from_email, accept_tos, tokenAddress, expires, success_callback, failure_callback, is_for_bounty_fulfiller, noAvailableUser) {
if (typeof web3 == 'undefined') {
_alert({ message: gettext('You must have a web3 enabled browser to do this. Please download Metamask.') }, 'warning');
failure_callback();
@@ -230,16 +230,19 @@ function sendTip(email, github_url, from_name, username, amount, comments_public
failure_callback();
return;
}
+
if (!isNumeric(amountInDenom) || amountInDenom == 0) {
- _alert({ message: gettext('You must enter an number for the amount!') }, 'warning');
+ _alert({ message: gettext('You must enter a number for the amount!') }, 'warning');
failure_callback();
return;
}
- if (username == '') {
+
+ if (username == '' && !noAvailableUser) {
_alert({ message: gettext('You must enter a username.') }, 'warning');
failure_callback();
return;
}
+
if (!accept_tos) {
_alert({ message: gettext('You must accept the terms.') }, 'warning');
failure_callback();
diff --git a/app/assets/v2/js/activity.js b/app/assets/v2/js/activity.js
index 5c35919cbc9..97a281b78f6 100644
--- a/app/assets/v2/js/activity.js
+++ b/app/assets/v2/js/activity.js
@@ -481,6 +481,37 @@ $(document).ready(function() {
});
+ $(document).on('click', '.award', function(e) {
+ e.preventDefault();
+ if (!document.contxt.github_handle) {
+ _alert('Please login first.', 'error');
+ return;
+ }
+
+ activityId = $(this).data('activity');
+ commentId = $(this).data('comment');
+
+ // remote post
+ var params = {
+ 'method': 'award',
+ 'comment': commentId,
+ 'csrfmiddlewaretoken': $('input[name=csrfmiddlewaretoken]').val()
+ };
+ var url = '/api/v0.1/activity/' + activityId;
+ var parent = $(this).parents('.row.box');
+
+ parent.find('.loading').removeClass('hidden');
+ $.post(url, params, function(response) {
+ // no message to be sent
+ $('button[data-activity=' + activityId + ']').remove();
+ parent.find('.loading').addClass('hidden');
+ _alert('Tip user successful!');
+ }).fail(function() {
+ parent.find('.error').removeClass('hidden');
+ });
+ });
+
+
// like activity
$(document).on('click', '.like_activity, .flag_activity, .favorite_activity, .pin_activity', function(e) {
e.preventDefault();
@@ -729,21 +760,28 @@ $(document).ready(function() {
the_comment = urlify(the_comment);
the_comment = linkify(the_comment);
+
the_comment = the_comment.replace(/\r\n|\r|\n/g, '
');
const timeAgo = timedifferenceCvrt(new Date(comment['created_on']));
const show_tip = true;
const is_comment_owner = document.contxt.github_handle == comment['profile_handle'];
+
+ const can_award = (
+ response.author === document.contxt.github_handle &&
+ response.has_tip == true &&
+ response.tip_available == true);
+ const can_redeem = response.can_redeem;
+
const is_edited = typeof comment['is_edited'] !== 'undefined' ? comment['is_edited'] : false;
+
var sorted_match_curve_html = '';
if (comment['sorted_match_curve']) {
-
var match_curve = Array.from(convert_to_dict(comment['sorted_match_curve']).values());
for (let j = 0; j < match_curve.length; j++) {
let ele = match_curve[j];
-
sorted_match_curve_html += '
';
sorted_match_curve_html += `Your contribution of ${ele.name} could yield $${Math.round(ele.value * 1000) / 1000} in matching.`;
sorted_match_curve_html += '';
@@ -827,6 +865,16 @@ $(document).ready(function() {
${the_comment}
+ ${can_award && `
+ ` || ''}
+ ${!!comment['redeem_link'] && can_redeem && `
+ Redeem tip` || ''}
+ ${!!comment['redeem_link'] && !can_redeem && `
+ Tip redeemed` || ''}
diff --git a/app/assets/v2/js/status.js b/app/assets/v2/js/status.js
index 1ac2ec5cbbf..a7e16334278 100644
--- a/app/assets/v2/js/status.js
+++ b/app/assets/v2/js/status.js
@@ -408,6 +408,13 @@ $(document).ready(function() {
}
});
+ $('#btn_attach').on('click', function() {
+ const el = $('#attach-dropdown');
+
+ el.toggle();
+ });
+
+
function submitStatusUpdate() {
if ($('#btn_post').is(':disabled')) {
return;
@@ -470,9 +477,16 @@ $(document).ready(function() {
data.append('image', image);
}
}
+
+ const attach = $('#attach-dropdown')[0].style.display;
+ const amount = $('#attachAmount').val();
+ const address = $('#attachToken').val();
+ const token_name = $('#attachToken :selected').text();
+
$('#bg-selector').attr('data-selected', null);
$('#bg-selector').addClass('d-none');
$('#bg-selector').children('div').children('div').addClass('d-none');
+
var fail_callback = function() {
message.val(the_message);
localStorage.setItem(lskey, the_message);
@@ -482,21 +496,108 @@ $(document).ready(function() {
);
};
- for (let i = 0; i < 5; i++) {
- const val = $('#poll_container input[name=option' + i + ']').val();
+ const success_callback = function(txid) {
+ const url = 'https://' + etherscanDomain() + '/tx/' + txid;
+ const msg = 'This payment has been sent đź‘Ś [Etherscan Link]';
+
+ _alert(msg, 'info', 1000);
+
+ data.append('attachTxId', txid);
+ fetch('/api/v0.1/activity', {
+ method: 'post',
+ body: data
+ }).then(response => {
+ if (response.status === 200) {
+ $('#thumbnail').hide();
+ $('#thumbnail-title').text('');
+ $('#thumbnail-provider').text('');
+ $('#thumbnail-desc').text('');
+ $('#thumbnail-img').attr('src', '');
+ $('#preview').hide();
+ $('#preview-img').attr('src', '');
+ $('#attach-dropdown').hide();
+ $('#attachAmount').val('');
+
+ embedded_resource = '';
+
+ _alert(
+ { message: gettext('Status has been saved.') },
+ 'success',
+ 1000
+ );
+ const activityContainer = document.querySelector('.tab-section.active .activities');
+
+ if (!activityContainer) {
+ document.run_long_poller(false);
+ // success
+ return;
+ }
+ activityContainer.setAttribute('page', 0);
+ $('.tab-section.active .activities').html('');
+ message.val('');
+ } else {
+ _alert(
+ { message: gettext('An error occurred. Please try again.') },
+ 'error'
+ );
+ }
+ }).catch(err => fail_callback());
+ };
+
+ const failure_callback = function() {
+ $.noop(); // do nothing
+ };
+
+ if (!isNaN(parseFloat(amount)) && address) {
+ data.append('attachToken', address);
+ data.append('attachAmount', amount);
+ data.append('attachTokenName', token_name);
+ const email = '';
+ const github_url = '';
+ const from_name = document.contxt['github_handle'];
+ const username = '';
+ const amountInEth = amount;
+ const comments_priv = '';
+ const comments_public = '';
+ const from_email = '';
+ const accept_tos = true;
+ const tokenAddress = address;
+ const expires = 9999999999;
+
+ sendTip(
+ email,
+ github_url,
+ from_name,
+ username,
+ amountInEth,
+ comments_public,
+ comments_priv,
+ from_email,
+ accept_tos,
+ tokenAddress,
+ expires,
+ success_callback,
+ failure_callback,
+ false,
+ true, // No available user to send tip at this moment
+ );
- if (val) {
- data.append('option' + i, val);
+ } else {
+ for (let i = 0; i < 5; i++) {
+ const val = $('#poll_container input[name=option' + i + ']').val();
+
+ if (val) {
+ data.append('option' + i, val);
+ }
}
- }
- $('#poll_container').remove();
- $('#video_container').remove();
- fetch('/api/v0.1/activity', {
- method: 'post',
- body: data
- })
- .then(response => {
+ $('#poll_container').remove();
+ $('#video_container').remove();
+
+ fetch('/api/v0.1/activity', {
+ method: 'post',
+ body: data
+ }).then(response => {
if (response.status === 200) {
$('#thumbnail').hide();
$('#thumbnail-title').text('');
@@ -505,6 +606,8 @@ $(document).ready(function() {
$('#thumbnail-img').attr('src', '');
$('#preview').hide();
$('#preview-img').attr('src', '');
+ $('#attach-dropdown').hide();
+ $('#attachAmount').val('');
embedded_resource = '';
_alert(
@@ -525,8 +628,8 @@ $(document).ready(function() {
} else {
fail_callback();
}
- })
- .catch(err => fail_callback());
+ }).catch(err => fail_callback());
+ }
}
});
diff --git a/app/dashboard/templates/profiles/status_box.html b/app/dashboard/templates/profiles/status_box.html
index 0e3f0c1f16b..510b08f5dc9 100644
--- a/app/dashboard/templates/profiles/status_box.html
+++ b/app/dashboard/templates/profiles/status_box.html
@@ -31,7 +31,9 @@
-
+
+
+
@@ -61,6 +63,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/marketing/mails.py b/app/marketing/mails.py
index 9e901fdd26e..89685678b48 100644
--- a/app/marketing/mails.py
+++ b/app/marketing/mails.py
@@ -485,11 +485,28 @@ def mention_email(post, to_emails):
if not should_suppress_notification_email(to_email, 'mention'):
send_mail(from_email, to_email, subject, text, html, categories=['notification', func_name()])
- finally:
- pass
+ except Exception as e:
+ logger.error('Status Update error - Error: (%s) - Handle: (%s)', e, to_email)
+
translation.activate(cur_language)
+def tip_comment_awarded_email(post, to_emails):
+ subject = gettext("🏆 @{} has awarded you.").format(post.profile.handle)
+ cur_language = translation.get_language()
+
+ for to_email in to_emails:
+ try:
+ setup_lang(to_email)
+ from_email = settings.CONTACT_EMAIL
+ html, text = render_mention(to_email, post)
+
+ if not should_suppress_notification_email(to_email, 'mention'):
+ send_mail(from_email, to_email, subject, text, html, categories=['notification', func_name()])
+ except Exception as e:
+ logger.error('Status Update error - Error: (%s) - Handle: (%s)', e, to_email)
+ translation.activate(cur_language)
+
def wall_post_email(activity):
diff --git a/app/retail/templates/shared/activity.html b/app/retail/templates/shared/activity.html
index 29148f1b744..745e63bd83c 100644
--- a/app/retail/templates/shared/activity.html
+++ b/app/retail/templates/shared/activity.html
@@ -388,10 +388,15 @@
{% endif %}
-
{% endif %}
+ {% if row.metadata.attach.token %}
+ |
+ {% endif %}
diff --git a/app/retail/views.py b/app/retail/views.py
index 99eccba8322..c1eaa98585c 100644
--- a/app/retail/views.py
+++ b/app/retail/views.py
@@ -42,7 +42,7 @@
from app.utils import get_default_network, get_profiles_from_text
from cacheops import cached_as, cached_view, cached_view_as
from dashboard.models import (
- Activity, Bounty, HackathonEvent, Profile, TribeMember, get_my_earnings_counter_profiles, get_my_grants,
+ Activity, Bounty, HackathonEvent, Profile, TribeMember, get_my_earnings_counter_profiles, get_my_grants, Tip,
)
from dashboard.notifications import amount_usdt_open_work, open_bounties
from dashboard.tasks import grant_update_email_task
@@ -1092,6 +1092,7 @@ def activity(request):
'pinned': None,
'target': f'/activity?what={what}&trending_only={trending_only}&page={next_page}',
'title': _('Activity Feed'),
+ 'TOKENS': request.user.profile.token_approvals.all() if request.user.is_authenticated else [],
'my_tribes': list(request.user.profile.tribe_members.values_list('org__handle',flat=True)) if request.user.is_authenticated else [],
}
context["activities"] = [a.view_props_for(request.user) for a in page]
@@ -1110,6 +1111,10 @@ def create_status_update(request):
resource = request.POST.get('resource', '')
provider = request.POST.get('resourceProvider', '')
resource_id = request.POST.get('resourceId', '')
+ attach_token = request.POST.get('attachToken', '')
+ attach_amount = request.POST.get('attachAmount', '')
+ attach_token_name = request.POST.get('attachTokenName', '')
+ tx_id = request.POST.get('attachTxId', '')
kwargs = {
'activity_type': 'status_update',
@@ -1125,6 +1130,15 @@ def create_status_update(request):
}
}
+ if tx_id:
+ kwargs['tip'] = Tip.objects.get(txid=tx_id)
+ amount = float(attach_amount)
+ kwargs['metadata']['attach'] = {
+ 'amount': amount,
+ 'token': attach_token,
+ 'token_name': attach_token_name,
+ }
+
if resource == 'content':
meta = kwargs['metadata']['resource']
meta['title'] = request.POST.get('title', '')
diff --git a/app/townsquare/migrations/0021_comment_tip.py b/app/townsquare/migrations/0021_comment_tip.py
new file mode 100644
index 00000000000..3f30a5bd580
--- /dev/null
+++ b/app/townsquare/migrations/0021_comment_tip.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.4 on 2020-05-26 14:29
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0114_auto_20200522_0730'),
+ ('townsquare', '0020_auto_20200521_1020'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='comment',
+ name='tip',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='awards', to='dashboard.Tip'),
+ ),
+ ]
diff --git a/app/townsquare/models.py b/app/townsquare/models.py
index f5b9ced4e8a..ffd20796aa0 100644
--- a/app/townsquare/models.py
+++ b/app/townsquare/models.py
@@ -57,6 +57,13 @@ class Comment(SuperModel):
activity = models.ForeignKey('dashboard.Activity',
on_delete=models.CASCADE, related_name='comments', blank=True, db_index=True)
comment = models.TextField(default='', blank=True)
+ tip = models.ForeignKey(
+ 'dashboard.Tip',
+ related_name='awards',
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True
+ )
likes = ArrayField(models.IntegerField(), default=list, blank=True) #pks of users who like this post
likes_handles = ArrayField(models.CharField(max_length=200, blank=True), default=list, blank=True) #handles of users who like this post
tip_count_eth = models.DecimalField(default=0, decimal_places=5, max_digits=50)
@@ -69,6 +76,16 @@ def __str__(self):
def profile_handle(self):
return self.profile.handle
+ @property
+ def redeem_link(self):
+ if self.tip:
+ return self.tip.receive_url
+ return ''
+
+ @property
+ def tip_able(self):
+ return self.activity.metadata.get("tip_able", False)
+
@property
def url(self):
return self.activity.url
diff --git a/app/townsquare/templates/townsquare/index.html b/app/townsquare/templates/townsquare/index.html
index 2717f552c1a..64b50f69bdc 100644
--- a/app/townsquare/templates/townsquare/index.html
+++ b/app/townsquare/templates/townsquare/index.html
@@ -215,6 +215,14 @@
We’re excited you’re here! Let’s ge
{% include 'shared/analytics.html' %}
{% include 'shared/footer_scripts.html' with slim=1 %}
{% include 'shared/activity_scripts.html' %}
+
+
+
+
+
+
+
+
diff --git a/app/townsquare/templates/townsquare/shared/shareactivity.html b/app/townsquare/templates/townsquare/shared/shareactivity.html
index 177d14a73a0..7d1a333ed73 100644
--- a/app/townsquare/templates/townsquare/shared/shareactivity.html
+++ b/app/townsquare/templates/townsquare/shared/shareactivity.html
@@ -40,7 +40,8 @@
-
+
+
@@ -59,6 +60,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/townsquare/views.py b/app/townsquare/views.py
index b1c2b5462e9..b4adb73ebc6 100644
--- a/app/townsquare/views.py
+++ b/app/townsquare/views.py
@@ -4,19 +4,24 @@
from django.conf import settings
from django.contrib import messages
from django.http import Http404, JsonResponse
-from django.shortcuts import redirect, render
+from django.shortcuts import redirect, render, get_object_or_404
from django.template.response import TemplateResponse
from django.utils import timezone
+from dashboard.models import Activity, HackathonEvent, get_my_earnings_counter_profiles, get_my_grants, Profile
import metadata_parser
+
from app.redis_service import RedisService
from dashboard.helpers import load_files_in_directory
from dashboard.models import (
Activity, HackathonEvent, Profile, TribeMember, get_my_earnings_counter_profiles, get_my_grants,
)
+
from kudos.models import Token
-from marketing.mails import comment_email, new_action_request
+
+from marketing.mails import comment_email, mention_email, new_action_request, tip_comment_awarded_email
from perftools.models import JSONStore
+
from ratelimit.decorators import ratelimit
from retail.views import get_specific_activities
@@ -422,6 +427,7 @@ def town_square(request):
'announcements': announcements,
'is_subscribed': is_subscribed,
'offers_by_category': offers_by_category,
+ 'TOKENS': request.user.profile.token_approvals.all() if request.user.is_authenticated else [],
'following_tribes': following_tribes,
'suggested_tribes': suggested_tribes,
'audience': audience
@@ -476,6 +482,13 @@ def api(request, activity_id):
# no perms needed responses go here
if request.GET.get('method') == 'comment':
comments = activity.comments.prefetch_related('profile').order_by('created_on')
+ # check for permissions
+ is_authenticated = request.user.is_authenticated
+ if request.POST.get('method') == 'delete':
+ has_perms = activity.profile == request.user.profile
+ if is_authenticated:
+ profile = request.user.profile
+
response['comments'] = []
results = {i : 0 for i in range(0, 15)}
for comment in comments:
@@ -508,19 +521,24 @@ def api(request, activity_id):
counter += 1; results[counter] += time.time() - start_time; start_time = time.time()
if comment.is_edited:
comment_dict['is_edited'] = comment.is_edited
+
+ comment_dict['redeem_link'] = comment.redeem_link if is_authenticated and comment.tip and comment.tip.recipient_profile_id == profile.id else ''
+ comment_dict['tip'] = bool(comment.tip)
response['comments'].append(comment_dict)
+
+ response['has_tip'] = False
+ if activity.tip:
+ user_can_redeem = request.user.profile.id == activity.tip.recipient_profile_id
+ response['has_tip'] = True
+ response['tip_available'] = not activity.tip.recipient_profile
+ response['can_redeem'] = activity.tip.status == 'PENDING' and user_can_redeem
+ response['author'] = activity.profile.handle
+
for key, val in results.items():
if settings.DEBUG:
print(key, round(val, 2))
return JsonResponse(response)
- # check for permissions
- has_perms = request.user.is_authenticated
- if request.POST.get('method') == 'delete':
- has_perms = activity.profile == request.user.profile
- if not has_perms:
- raise Http404
-
# deletion request
if request.POST.get('method') == 'delete':
activity.delete()
@@ -555,6 +573,18 @@ def api(request, activity_id):
if request.POST['direction'] == 'unliked':
activity.likes.filter(profile=request.user.profile).delete()
+ # award request
+ elif request.POST.get('method') == 'award':
+ comment = get_object_or_404(Comment, id=int(request.POST['comment']))
+ if request.user.profile.id == activity.profile.id and comment.activity_id == activity.id:
+ recipient_profile = comment.profile
+ activity.tip.username = recipient_profile.username
+ activity.tip.recipient_profile = recipient_profile
+ activity.tip.save()
+ comment.tip = activity.tip
+ comment.save()
+ tip_comment_awarded_email(comment, [recipient_profile.email])
+
# favorite request
elif request.POST.get('method') == 'favorite':
if request.POST['direction'] == 'favorite':