diff --git a/app/event.rb b/app/event.rb index 7c4395c0..a1d6eecf 100644 --- a/app/event.rb +++ b/app/event.rb @@ -13,18 +13,28 @@ post '/event/:event_id/comment' do |event_id| content_type :json event = Event[id: event_id] - return 403 if event.site && event.site.is_blocking?(current_site) - return 403 if event.actioning_site && event.actioning_site.is_blocking?(current_site) + if event.site && event.site.is_blocking?(current_site) + flash[:error] = comment_unavailable_message(event.site) + return {result: 'error', message: flash[:error]}.to_json + end + + if event.actioning_site && event.actioning_site.is_blocking?(current_site) + flash[:error] = comment_unavailable_message(event.actioning_site) + return {result: 'error', message: flash[:error]}.to_json + end site = event.site message = normalize_comment_message(params[:message]) + message_error = comment_message_error(message) + unavailable_message = comment_unavailable_message(site) if(site.is_blocking?(current_site) || site.profile_comments_enabled == false || current_site.commenting_allowed? == false || (current_site.is_a_jerk? && event.site_id != current_site.id && !site.is_following?(current_site)) || - !valid_comment_message?(message)) - return {result: 'error'}.to_json + message_error) + flash[:error] = message_error || unavailable_message + return {result: 'error', message: flash[:error]}.to_json end event.add_site_comment current_site, message @@ -36,8 +46,9 @@ post '/event/:event_id/update_profile_comment' do |event_id| content_type :json event = Event[id: event_id] message = normalize_comment_message(params[:message]) + message_error = comment_message_error(message) return {result: 'error'}.to_json unless (current_site.id == event.profile_comment.actioning_site_id && - valid_comment_message?(message)) + message_error.nil?) event.profile_comment.update message: message return {result: 'success'}.to_json diff --git a/app/site.rb b/app/site.rb index 9c7dcba0..8e5af540 100644 --- a/app/site.rb +++ b/app/site.rb @@ -165,21 +165,27 @@ post '/site/:username/comment' do |username| site = Site[username: username] message = normalize_comment_message(params[:message]) + message_error = comment_message_error(message) - redirect request.referer if current_site && (site.is_blocking?(current_site) || current_site.is_blocking?(site)) + if current_site && (site.is_blocking?(current_site) || current_site.is_blocking?(site)) + flash[:error] = comment_unavailable_message(site) + redirect request.referer + end last_comment = site.profile_comments_dataset.order(:created_at.desc).first if last_comment && last_comment.message == message && last_comment.created_at > 2.hours.ago + flash[:error] = 'You already posted that comment.' redirect request.referer end if site.profile_comments_enabled == false || - !valid_comment_message?(message) || + message_error || site.is_blocking?(current_site) || current_site.is_blocking?(site) || current_site.commenting_allowed? == false || (current_site.is_a_jerk? && site.id != current_site.id && !site.is_following?(current_site)) + flash[:error] = message_error || comment_unavailable_message(site) redirect request.referrer end diff --git a/app_helpers.rb b/app_helpers.rb index 53a666f1..0c78d76f 100644 --- a/app_helpers.rb +++ b/app_helpers.rb @@ -131,21 +131,66 @@ def sanitize_comment(text) Rinku.auto_link Sanitize.fragment(text), :all, 'target="_blank" rel="nofollow"' end +def flash_message_keys + flash.keys.select {|k| [:success, :error, :errors].include?(k) && !flash[k].to_s.empty?} +end + +def flash_message_entries + entries = [] + + Array(@error).each do |message| + entries << {type: 'error', message: message} unless message.to_s.empty? + end + + flash_message_keys.each do |key| + type = key == :success ? 'success' : 'error' + + Array(flash[key]).each do |message| + entries << {type: type, message: message} unless message.to_s.empty? + end + end + + entries +end + def normalize_comment_message(message) message.to_s.gsub(/\r\n?/, "\n").gsub(/\n{3,}/, "\n\n").strip end -def valid_comment_message?(message) - return false if message.empty? || message.length > Site::MAX_COMMENT_SIZE +def comment_message_error(message) + return 'Comment cannot be empty.' if message.empty? + return "Comments must be #{Site::MAX_COMMENT_SIZE} characters or fewer." if message.length > Site::MAX_COMMENT_SIZE lines = message.split("\n") - return false if lines.length > Site::MAX_COMMENT_LINES + return "Comments must be #{Site::MAX_COMMENT_LINES} lines or fewer." if lines.length > Site::MAX_COMMENT_LINES - return true if lines.length < Site::MULTILINE_COMMENT_AVERAGE_LINE_THRESHOLD + return nil if lines.length < Site::MULTILINE_COMMENT_AVERAGE_LINE_THRESHOLD nonblank_lines = lines.map(&:strip).reject(&:empty?) + return 'Comment cannot be empty.' if nonblank_lines.empty? + average_line_length = nonblank_lines.sum(&:length).to_f / nonblank_lines.length - average_line_length >= Site::MIN_MULTILINE_COMMENT_AVERAGE_LINE_LENGTH + return nil if average_line_length >= Site::MIN_MULTILINE_COMMENT_AVERAGE_LINE_LENGTH + + 'Multiline comments need a little more text on each line.' +end + +def valid_comment_message?(message) + comment_message_error(message).nil? +end + +def comment_unavailable_message(site=nil) + return 'Comments are disabled for this site.' if site && site.profile_comments_enabled == false + + if current_site && !current_site.commenting_allowed? + if current_site.commenting_too_much? + return "To prevent spam, comments are limited to #{Site::MAX_COMMENTS_PER_DAY} per day. Please try again tomorrow." + end + + return "To prevent spam, you cannot comment until you have updated your site #{Site::COMMENTING_ALLOWED_UPDATED_COUNT} times on separate days, and your account is one week old." + end + + 'You cannot comment on this right now.' end def flash_display(opts={}) diff --git a/public/js/news/profile_comment.js b/public/js/news/profile_comment.js index 21e535cc..89b654a2 100644 --- a/public/js/news/profile_comment.js +++ b/public/js/news/profile_comment.js @@ -1,31 +1,45 @@ var ProfileComment = { + commentContent: function(eventId) { + return $('#event_'+eventId+' > div.content').first() + }, + displayEditor: function(eventId) { - var commentDiv = $('#event_'+eventId+' div.content').first() + var commentDiv = this.commentContent(eventId) var eventActions = $('#event_'+eventId+'_actions').first() + var rendered = Template.template($('#comment-edit-template').html(), { + eventId: eventId, + content: commentDiv.text() + }) eventActions.find('a#editLink').css('display', 'none') - commentDiv.html(Template.template($('#comment-edit-template').html(), {eventId: eventId, content: commentDiv.text()})) - $('#event_'+eventId+' div.title div.comment').text() + commentDiv.data('original-html', commentDiv.html()) + commentDiv.addClass('is-editing') + commentDiv.html($.trim(rendered)) + commentDiv.find('textarea').focus() }, cancelEditor: function(eventId) { var eventActions = $('#event_'+eventId+'_actions').first() - var commentDiv = $('#event_'+eventId+' div.content').first() + var commentDiv = this.commentContent(eventId) eventActions.find('a#editLink').css('display', 'inline') - commentDiv.text(commentDiv.find('textarea').text()) + commentDiv.removeClass('is-editing') + commentDiv.html(commentDiv.data('original-html')) }, update: function(eventId, csrfToken) { - var commentDiv = $('#event_'+eventId+' div.content').first() - var self = this - console.log(commentDiv.find('textarea').val()) + var commentDiv = this.commentContent(eventId) + var message = commentDiv.find('textarea').val() + $.post('/event/'+eventId+'/update_profile_comment', { csrf_token: csrfToken, - message: commentDiv.find('textarea').val() + message: message }, function(res) { - commentDiv.find('textarea').text(commentDiv.find('textarea').val()) - self.cancelEditor(eventId) + if(res.result == 'success') { + $('#event_'+eventId+'_actions').first().find('a#editLink').css('display', 'inline') + commentDiv.removeClass('is-editing') + commentDiv.text(message) + } }) } } diff --git a/sass/_project-sass/_project-Main.scss b/sass/_project-sass/_project-Main.scss index bd294a5c..7abd8462 100644 --- a/sass/_project-sass/_project-Main.scss +++ b/sass/_project-sass/_project-Main.scss @@ -13,7 +13,7 @@ } } .content, .footer-Content { - padding: 20px 3%; + padding: 20px 3%; @media (max-device-width:480px), screen and (max-width:800px) { padding: 20px 7%; @@ -23,6 +23,126 @@ padding: 20px 0; } } + +.flash-Messages { + box-sizing: border-box; + left: 0; + padding: 0 3%; + pointer-events: none; + position: fixed; + right: 0; + top: 72px; + z-index: 2100; + + @media (max-device-width:480px), screen and (max-width:800px) { + padding: 0 7%; + top: 62px; + } +} + +.hp .flash-Messages { + top: 62px; +} + +.flash-Message { + align-items: flex-start; + background: #f9fffd; + border: 1px solid #d6e9e6; + border-left: 4px solid $c-Brand-1; + border-radius: 4px; + box-shadow: 0 1px 2px rgba(44, 62, 80, .08); + box-sizing: border-box; + color: #2c3e50; + display: flex; + gap: 10px; + line-height: 1.4; + margin: 0 auto 14px auto; + max-width: 1200px; + animation: flashMessageFadeOut .25s ease 5s forwards; + pointer-events: auto; + padding: 11px 12px; + + &:hover, &:focus-within { + animation-play-state: paused; + } +} + +.flash-Message--error { + background: #fff8ed; + border-color: #ead7bd; + border-left-color: #d9820f; +} + +.flash-Message--success { + background: #f2fbf8; + border-color: #cfe9df; + border-left-color: #18bc9c; +} + +.flash-MessageIcon { + color: #18bc9c; + flex: 0 0 18px; + line-height: 1.4; + padding-top: 1px; + text-align: center; +} + +.flash-Message--error .flash-MessageIcon { + color: #d9820f; +} + +.flash-MessageText { + flex: 1 1 auto; + min-width: 0; + + p { + margin: 0; + overflow-wrap: anywhere; + } +} + +.flash-MessageClose { + background: transparent; + border: 0; + color: #7b8a8b; + flex: 0 0 auto; + font-size: 13px; + line-height: 1; + margin: -1px -3px -1px 6px; + padding: 4px; + + &:hover, &:focus { + color: #2c3e50; + } +} + +.flash-Messages.is-hidden, .flash-Message.is-hidden { + display: none; +} + +@keyframes flashMessageFadeOut { + 0% { + opacity: 1; + transform: translateY(0); + visibility: visible; + } + + 99% { + opacity: 0; + transform: translateY(-4px); + visibility: visible; + } + + 100% { + opacity: 0; + transform: translateY(-4px); + visibility: hidden; + } +} + +.flash-Messages--centered .flash-MessageText { + text-align: center; +} .single-Col{ } @@ -1439,6 +1559,30 @@ a.tag:hover { .news-item.comment > .title + .content { white-space: pre-wrap; } +.news-item.comment > .title + .content.is-editing { + white-space: normal; +} +.news-item.comment .profile-comment-editor { + margin: 0; +} +.news-item.comment .profile-comment-editor textarea { + box-sizing: border-box; + display: block; + font-size: 1em; + line-height: 1.4; + margin: 0 0 8px 0; + min-height: 82px; + resize: vertical; + width: 100%; +} +.news-item.comment .profile-comment-editor-actions { + align-items: center; + display: flex; + gap: 10px; +} +.news-item.comment .profile-comment-editor-actions .btn-Action { + margin: 0; +} .news-item > .title + .actions { margin-top: 8px; } diff --git a/tests/site_change_tests.rb b/tests/site_change_tests.rb new file mode 100644 index 00000000..bbd17bc6 --- /dev/null +++ b/tests/site_change_tests.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require_relative './environment' + +describe SiteChange do + it 'returns up to ten changed filenames by default' do + site = Fabricate :site + site_change = SiteChange.create site: site + base_time = Time.now + + 11.times do |i| + site_change.add_site_change_file site_id: site.id, filename: "page#{i}.html", created_at: base_time + i + end + + filenames = site_change.site_change_filenames + + _(filenames.length).must_equal 10 + _(filenames).must_include 'page10.html' + _(filenames).wont_include 'page0.html' + end +end diff --git a/views/_flash.erb b/views/_flash.erb index 7af68ecf..6a40738f 100644 --- a/views/_flash.erb +++ b/views/_flash.erb @@ -1,15 +1,19 @@ -<% if @error %> -
-

<%== @error %>

-
-<% end %> - -<% if flash.keys.length > 0 %> -
-

- <% flash.keys.select {|k| [:success, :error, :errors].include?(k)}.each do |key| %> - <%== flash[key] %> - <% end %> -

+<% opts ||= {} %> +<% messages = flash_message_entries %> +<% if messages.any? %> +
+ <% messages.each do |entry| %> +
+ +
+

<%== entry[:message] %>

+
+ +
+ <% end %>
<% end %> diff --git a/views/_news_templates.erb b/views/_news_templates.erb index 8acc91b0..d2db9a35 100644 --- a/views/_news_templates.erb +++ b/views/_news_templates.erb @@ -6,8 +6,11 @@
diff --git a/views/admin.erb b/views/admin.erb index bebdd7f6..d5f3c458 100644 --- a/views/admin.erb +++ b/views/admin.erb @@ -11,9 +11,6 @@ Site Reports | Site Bandwidth Usage | Site Ban History - - <%== flash_display %> -

Ban Site

diff --git a/views/admin/ban_history.erb b/views/admin/ban_history.erb index d84d9b54..d5d792d2 100644 --- a/views/admin/ban_history.erb +++ b/views/admin/ban_history.erb @@ -6,8 +6,6 @@
- <%== flash_display %> - <% if @sites.empty? %>

No sites found.

@@ -218,4 +216,4 @@ }); }); }); - \ No newline at end of file + diff --git a/views/admin/email.erb b/views/admin/email.erb index 44238c98..53603310 100644 --- a/views/admin/email.erb +++ b/views/admin/email.erb @@ -6,9 +6,6 @@
- - <%== flash_display %> -

diff --git a/views/admin/reports.erb b/views/admin/reports.erb index 58ee8110..855f5e89 100644 --- a/views/admin/reports.erb +++ b/views/admin/reports.erb @@ -6,8 +6,6 @@

- <%== flash_display %> - <% if @reports.empty? %>

No reports found.

diff --git a/views/admin/site.erb b/views/admin/site.erb index 2a57c2ec..8f379ddd 100644 --- a/views/admin/site.erb +++ b/views/admin/site.erb @@ -14,7 +14,6 @@
- <%== flash_display %>
diff --git a/views/dashboard/index.erb b/views/dashboard/index.erb index 7ce43ad3..22175817 100644 --- a/views/dashboard/index.erb +++ b/views/dashboard/index.erb @@ -61,15 +61,7 @@
<% end %> -<% if flash.keys.length > 0 %> -
- <% flash.keys.select {|k| [:success, :error, :errors].include?(k)}.each do |key| %> - <%== flash[key] %> - <% end %> -
-<% else %> - -<% end %> +
<%== erb :'dashboard/files' %> diff --git a/views/forgot_username.erb b/views/forgot_username.erb index 7f7f966c..d40c894d 100644 --- a/views/forgot_username.erb +++ b/views/forgot_username.erb @@ -6,8 +6,6 @@
- <%== flash_display %> -

If you forgot your sitename (AKA username), you can have it emailed to you here.

Keep in mind that we cannot help you locate your sitename without a valid email address, and we don't disclose whether an email address exists in our system. This is for security and privacy reasons.

diff --git a/views/layout.erb b/views/layout.erb index 3fb82e2a..6bb0aeb8 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -39,6 +39,7 @@
<%== erb :'_header', layout: false %> + <%== flash_display %> <%== yield %>
diff --git a/views/password_reset.erb b/views/password_reset.erb index cc5a750c..27db098b 100644 --- a/views/password_reset.erb +++ b/views/password_reset.erb @@ -5,9 +5,6 @@
- - <%== flash_display centered: true %> -
diff --git a/views/settings/account.erb b/views/settings/account.erb index 327c486d..44c6a62b 100644 --- a/views/settings/account.erb +++ b/views/settings/account.erb @@ -8,19 +8,6 @@
-
- <% if flash[:success] %> -
- <%== flash[:success] %> -
- <% end %> - - <% if flash[:error] %> -
- <%== flash[:error] %> -
- <% end %> -
\ No newline at end of file +
diff --git a/views/settings/site.erb b/views/settings/site.erb index 8066663d..17cbd07c 100644 --- a/views/settings/site.erb +++ b/views/settings/site.erb @@ -8,19 +8,6 @@
-
- <% if flash[:success] %> -
- <%== flash[:success] %> -
- <% end %> - - <% if flash[:error] %> -
- <%== flash[:error] %> -
- <% end %> -
- <%== flash_display %> -
diff --git a/views/signin/restore.erb b/views/signin/restore.erb index 16110650..e146445c 100644 --- a/views/signin/restore.erb +++ b/views/signin/restore.erb @@ -6,8 +6,6 @@
- <%== flash_display %> -

Your site was deleted.

Would you like to restore your site?

<%= @site.username %>.neocities.org

diff --git a/views/site.erb b/views/site.erb index ab199c0c..53c77548 100644 --- a/views/site.erb +++ b/views/site.erb @@ -1,13 +1,4 @@
- <% if current_site && flash.keys.length > 0 %> -
-
- <% flash.keys.each do |key| %> - <%= flash[key] %> - <% end %> -
-
- <% end %>
-<% end %> \ No newline at end of file +<% end %> diff --git a/views/site/confirm_email.erb b/views/site/confirm_email.erb index e720cc84..15de2214 100644 --- a/views/site/confirm_email.erb +++ b/views/site/confirm_email.erb @@ -32,19 +32,6 @@
- - <% if flash[:success] %> -
- <%== flash[:success] %> -
- <% end %> - - <% if flash[:error] %> -
- <%== flash[:error] %> -
- <% end %> -
<%== csrf_token_input_html %>
diff --git a/views/site/confirm_phone.erb b/views/site/confirm_phone.erb index 937973e4..68ada238 100644 --- a/views/site/confirm_phone.erb +++ b/views/site/confirm_phone.erb @@ -9,18 +9,6 @@
- <% if flash[:success] %> -
- <%== flash[:success] %> -
- <% end %> - - <% if flash[:error] %> -
- <%== flash[:error] %> -
- <% end %> - <%== csrf_token_input_html %> diff --git a/views/welcome.erb b/views/welcome.erb index b4f814c5..4e99a3c0 100644 --- a/views/welcome.erb +++ b/views/welcome.erb @@ -1,8 +1,4 @@
- <% if current_site %> - <%== flash_display centered: true %> - <% end %> - <% if request.path == '/welcome' %>

Welcome to Neocities, <%= current_site.username %>!

<% elsif parent_site && parent_site.supporter? %> @@ -130,4 +126,4 @@ placement: 'top' }) }) - \ No newline at end of file +