From 5ae1b39ec9b4d5269d2f01aeaa4304252b694519 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 18 Dec 2016 19:47:11 +0100 Subject: [PATCH] Adjusting public display of statuses to look similar to logged-in UI, fix #361 with rich OEmbed display via iframe, fix #237 by hiding sensitive content behind a spoiler on public pages --- app/assets/javascripts/extras.jsx | 14 +- app/assets/stylesheets/stream_entries.scss | 405 ++++++++++-------- app/controllers/api/oembed_controller.rb | 4 +- app/controllers/stream_entries_controller.rb | 5 +- app/helpers/stream_entries_helper.rb | 4 + app/views/api/oembed/show.json.rabl | 2 +- .../stream_entries/_content_spoiler.html.haml | 3 + .../stream_entries/_detailed_status.html.haml | 36 ++ .../stream_entries/_simple_status.html.haml | 28 ++ app/views/stream_entries/_status.html.haml | 25 +- app/views/stream_entries/embed.html.haml | 2 +- config/i18n-tasks.yml | 1 + config/locales/en.yml | 19 + 13 files changed, 339 insertions(+), 209 deletions(-) create mode 100644 app/views/stream_entries/_content_spoiler.html.haml create mode 100644 app/views/stream_entries/_detailed_status.html.haml create mode 100644 app/views/stream_entries/_simple_status.html.haml diff --git a/app/assets/javascripts/extras.jsx b/app/assets/javascripts/extras.jsx index 93f827044..9fd769c0b 100644 --- a/app/assets/javascripts/extras.jsx +++ b/app/assets/javascripts/extras.jsx @@ -1,8 +1,20 @@ import emojify from './components/emoji' $(() => { - $.each($('.entry .content, .name, .account__header__content'), (_, content) => { + $.each($('.entry .content, .entry .status__content, .display-name, .name, .account__header__content'), (_, content) => { const $content = $(content); $content.html(emojify($content.html())); }); + + $('.video-player video').on('click', e => { + if (e.target.paused) { + e.target.play(); + } else { + e.target.pause(); + } + }); + + $('.media-spoiler').on('click', e => { + $(e.target).hide(); + }); }); diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index 4df03b794..5cd140aac 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -3,232 +3,281 @@ box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); .entry { - border-bottom: 1px solid #d9e1e8; - background: #fff; - border-left: 2px solid #fff; - - &.entry-reblog { - border-left-color: #2b90d9; + .status.light, .detailed-status.light { + border-bottom: 1px solid #d9e1e8; } - &.entry-predecessor, &.entry-successor { - background: #d9e1e8; - border-left-color: #d9e1e8; - border-bottom-color: darken(#d9e1e8, 10%); + &:last-child { + .status.light, .detailed-status.light { + border-bottom: 0; + border-radius: 0 0 4px 4px; + } + } - .header { - .header__right { - .counter-btn { - color: darken(#d9e1e8, 15%); - } + &:first-child { + .status.light, .detailed-status.light { + border-radius: 4px 4px 0 0; + } + + &:last-child { + .status.light, .detailed-status.light { + border-radius: 4px; + } + } + } + } + + .status.light { + padding: 14px 14px 14px (48px + 14px*2); + position: relative; + min-height: 48px; + cursor: default; + background: lighten(#d9e1e8, 8%); + + .status__header { + font-size: 15px; + + .status__meta { + float: right; + font-size: 14px; + + .status__relative-time { + color: #9baec8; } } } - &.entry-center { - border-bottom-color: darken(#d9e1e8, 10%); + .status__display-name { + display: block; + max-width: 100%; + padding-right: 25px; + color: #282c37; } - &.entry-follow, &.entry-favourite { - .content { - padding-top: 10px; - padding-bottom: 10px; + .status__avatar { + position: absolute; + left: 14px; + top: 14px; + width: 48px; + height: 48px; + + & > div { + width: 48px; + height: 48px; + } + + img { + display: block; + border-radius: 4px; + } + } + + .display-name { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + strong { + font-weight: 500; + color: #282c37; + } + + span { + font-size: 14px; + color: #9baec8; + } + } + + .status__content { + color: #282c37; + + a { + color: #2b90d9; + } + } + + .status__attachments { + margin-top: 8px; + overflow: hidden; + width: 100%; + box-sizing: border-box; + height: 110px; + display: flex; + } + } + + .detailed-status.light { + padding: 14px; + background: #fff; + cursor: default; + + .detailed-status__display-name { + display: block; + overflow: hidden; + margin-bottom: 15px; + + & > div { + float: left; + margin-right: 10px; + } + + .display-name { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; strong { font-weight: 500; + color: #282c37; + } + + span { + font-size: 14px; + color: #9baec8; } } } - &:last-child { - border-bottom: 0; - border-radius: 0 0 4px 4px; - } - } + .avatar { + width: 48px; + height: 48px; - .entry:first-child { - border-radius: 4px 4px 0 0; - - &:last-child { - border-radius: 4px; - } - } - - @media screen and (max-width: 700px) { - border-radius: 0; - box-shadow: none; - - .entry { - &:last-child { - border-radius: 0; - } - - &:first-child { - border-radius: 0; - - &:last-child { - border-radius: 0; - } + img { + display: block; + border-radius: 4px; } } - } - .entry__container { - overflow: hidden; - } + .status__content { + color: #282c37; - .avatar { - width: 56px; - padding: 15px 10px; - padding-right: 5px; - float: left; - - img { - width: 56px; - height: 56px; - display: block; - border-radius: 4px; - } - } - - .entry__container__container { - margin-left: 71px; - } - - .header { - margin-bottom: 10px; - padding: 15px; - padding-bottom: 0; - padding-left: 8px; - display: flex; - - .header__left { - flex: 1; + a { + color: #2b90d9; + } } - .header__right { - - } - - .name { - text-decoration: none; + .detailed-status__meta { + margin-top: 15px; color: #9baec8; + font-size: 14px; + line-height: 18px; - strong { - color: #282c37; + a { + color: inherit; + } + + span > span { font-weight: 500; + font-size: 12px; + margin-left: 6px; + display: inline-block; } + } - &:hover { - strong { - text-decoration: underline; - } + .detailed-status__attachments { + margin-top: 8px; + overflow: hidden; + width: 100%; + box-sizing: border-box; + height: 300px; + display: flex; + } + + .video-player { + margin-top: 8px; + height: 300px; + overflow: hidden; + + video { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + top: 50%; + transform: translateY(-50%); } } } - .pre-header { - border-bottom: 1px solid #d9e1e8; - color: #2b90d9; - padding: 5px 10px; - padding-left: 8px; - clear: both; + .media-item, .video-item { + box-sizing: border-box; + position: relative; + left: auto; + top: auto; + right: auto; + bottom: auto; + float: left; + border: medium none; + display: block; + flex: 1 1 auto; + height: 100%; + margin-right: 2px; - .name { - color: #2b90d9; - font-weight: 500; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - } - - .content { - font-size: 14px; - padding: 0 15px; - padding-left: 8px; - padding-bottom: 15px; - color: #282c37; - word-wrap: break-word; - overflow: hidden; - white-space: pre-wrap; - - p { - margin-bottom: 18px; - - &:last-child { - margin-bottom: 0; - } + &:last-child { + margin-right: 0; } a { - color: #2b90d9; + display: block; + width: 100%; + height: 100%; + background: no-repeat scroll center center / cover; text-decoration: none; - - &:hover { - text-decoration: underline; - } - - &.mention { - &:hover { - text-decoration: none; - - span { - text-decoration: underline; - } - } - } + cursor: zoom-in; } } - .time { - text-decoration: none; - color: #9baec8; + .video-item { + max-width: 196px; + + a { + cursor: pointer; + } + + .video-item__play { + position: absolute; + top: 50%; + left: 50%; + font-size: 36px; + transform: translate(-50%, -50%); + padding: 5px; + border-radius: 100px; + color: rgba(255, 255, 255, 0.8); + } + } + + .media-spoiler { + background: #9baec8; + width: 100%; + height: 100%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + transition: all 100ms linear; &:hover { - text-decoration: underline; + background: darken(#9baec8, 5%); } - } - .media-attachments { - list-style: none; - margin: 0; - padding: 0; - display: block; - overflow: hidden; - padding-left: 10px; - margin-bottom: 15px; - - li { + span { display: block; - float: left; - width: 120px; - height: 100px; - border-radius: 4px; - margin-right: 4px; - margin-bottom: 4px; - a { - display: block; - width: 120px; - height: 100px; - border-radius: 4px; - background-position: center; - background-repeat: none; - background-size: cover; + &:first-child { + font-size: 14px; } - } - } - @media screen and (max-width: 360px) { - .avatar { - display: none; - } - - .entry__container__container { - margin-left: 7px; + &:last-child { + font-size: 11px; + font-weight: 500; + } } } } diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index d30ae8152..2360061ff 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -5,8 +5,8 @@ class Api::OembedController < ApiController def show @stream_entry = stream_entry_from_url(params[:url]) - @width = [300, params[:maxwidth].to_i].max - @height = [200, params[:maxheight].to_i].max + @width = params[:maxwidth].present? ? params[:maxwidth].to_i : 400 + @height = params[:maxheight].present? ? params[:maxheight].to_i : 600 end private diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 933bdf737..58dd423f7 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -9,8 +9,6 @@ class StreamEntriesController < ApplicationController before_action :check_account_suspension def show - @type = @stream_entry.activity_type.downcase - respond_to do |format| format.html do return gone if @stream_entry.activity.nil? @@ -27,7 +25,7 @@ class StreamEntriesController < ApplicationController def embed response.headers['X-Frame-Options'] = 'ALLOWALL' - @type = @stream_entry.activity_type.downcase + @external_links = true return gone if @stream_entry.activity.nil? @@ -46,6 +44,7 @@ class StreamEntriesController < ApplicationController def set_stream_entry @stream_entry = @account.stream_entries.find(params[:id]) + @type = @stream_entry.activity_type.downcase end def check_account_suspension diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 0aa7008be..5cd65008e 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -5,6 +5,10 @@ module StreamEntriesHelper account.display_name.blank? ? account.username : account.display_name end + def acct(account) + "@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}" + end + def avatar_for_status_url(status) status.reblog? ? status.reblog.account.avatar.url( :original) : status.account.avatar.url( :original) end diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl index 2bec9165e..f33b70ee5 100644 --- a/app/views/api/oembed/show.json.rabl +++ b/app/views/api/oembed/show.json.rabl @@ -9,6 +9,6 @@ node(:author_url) { |entry| account_url(entry.account) } node(:provider_name) { Rails.configuration.x.local_domain } node(:provider_url) { root_url } node(:cache_age) { 86_400 } -node(:html) { |entry| "
" } +node(:html) { |entry| "" } node(:width) { @width } node(:height) { nil } diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml new file mode 100644 index 000000000..d80ea46a0 --- /dev/null +++ b/app/views/stream_entries/_content_spoiler.html.haml @@ -0,0 +1,3 @@ +.media-spoiler + %span= t('stream_entries.sensitive_content') + %span= t('stream_entries.click_to_show') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml new file mode 100644 index 000000000..94451d3bd --- /dev/null +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -0,0 +1,36 @@ +.detailed-status.light + = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name', target: @external_links ? '_blank' : nil, rel: 'noopener' do + %div + %div.avatar + = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '' + %span.display-name + %strong= display_name(status.account) + %span= acct(status.account) + + .status__content= Formatter.instance.format(status) + + - unless status.media_attachments.empty? + - if status.media_attachments.first.video? + .video-player + - if status.sensitive? + = render partial: 'stream_entries/content_spoiler' + %video{ src: status.media_attachments.first.file.url(:original), loop: true } + - else + .detailed-status__attachments + - if status.sensitive? + = render partial: 'stream_entries/content_spoiler' + - status.media_attachments.each do |media| + .media-item + = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener' + + %div.detailed-status__meta + = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: @external_links ? '_blank' : nil, rel: 'noopener' do + %span= l(status.created_at) + · + %span + = fa_icon('retweet') + %span= status.reblogs.count + · + %span + = fa_icon('star') + %span= status.favourites.count diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml new file mode 100644 index 000000000..da3bc0ccb --- /dev/null +++ b/app/views/stream_entries/_simple_status.html.haml @@ -0,0 +1,28 @@ +.status.light + .status__header + .status__meta + = link_to time_ago_in_words(status.created_at), TagManager.instance.url_for(status), class: 'status__relative-time', title: l(status.created_at), target: @external_links ? '_blank' : nil, rel: 'noopener' + + = link_to TagManager.instance.url_for(status.account), class: 'status__display-name', target: @external_links ? '_blank' : nil, rel: 'noopener' do + .status__avatar + %div + = image_tag status.account.avatar(:original), width: 48, height: 48, alt: '' + %span.display-name + %strong= display_name(status.account) + %span= acct(status.account) + + .status__content= Formatter.instance.format(status) + + - unless status.media_attachments.empty? + .status__attachments + - if status.sensitive? + = render partial: 'stream_entries/content_spoiler' + - if status.media_attachments.first.video? + .video-item + = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener' do + .video-item__play + = fa_icon('play') + - else + - status.media_attachments.each do |media| + .media-item + = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener' diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 8169b8178..67cb06a83 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -1,7 +1,7 @@ - include_threads ||= false - is_predecessor ||= false - is_successor ||= false -- centered = include_threads && !is_predecessor && !is_successor +- centered ||= include_threads && !is_predecessor && !is_successor - if status.reply? && include_threads = render partial: 'status', collection: @ancestors, as: :status, locals: { is_predecessor: true } @@ -13,28 +13,7 @@ Shared by = link_to display_name(status.account), TagManager.instance.url_for(status.account), class: 'name' - .entry__container - .avatar - = image_tag avatar_for_status_url(status) - - .entry__container__container - .header - .header__left - = link_to TagManager.instance.url_for(proper_status(status).account), class: 'name' do - %strong= display_name(proper_status(status).account) - = "@#{proper_status(status).account.acct}" - - .header__right - = link_to TagManager.instance.url_for(proper_status(status)), class: 'time' do - %span{ title: proper_status(status).created_at } - = relative_time(proper_status(status).created_at) - - .content= Formatter.instance.format(proper_status(status)) - - - if (status.reblog? ? status.reblog : status).media_attachments.size > 0 - %ul.media-attachments - - (status.reblog? ? status.reblog : status).media_attachments.each do |media| - %li.transparent-background= link_to '', media.file.url( :original), style: "background-image: url(#{media.file.url( :small)})", target: '_blank' + = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) } - if include_threads = render partial: 'status', collection: @descendants, as: :status, locals: { is_successor: true } diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml index 4a733d428..fd07fdd91 100644 --- a/app/views/stream_entries/embed.html.haml +++ b/app/views/stream_entries/embed.html.haml @@ -1,2 +1,2 @@ .activity-stream.activity-stream-headless - = render partial: @type, locals: { @type.to_sym => @stream_entry.activity } + = render partial: @type, locals: { @type.to_sym => @stream_entry.activity, centered: true } diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 4dc6985b7..e72063844 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -33,6 +33,7 @@ search: ignore_unused: - 'activerecord.attributes.*' - '{devise,will_paginate,doorkeeper}.*' + - '{datetime,time}.*' - 'simple_form.{yes,no}' - 'simple_form.{placeholders,hints,labels}.*' - 'simple_form.{error_notification,required}.:' diff --git a/config/locales/en.yml b/config/locales/en.yml index 50a1f0e95..f58ce9a71 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -26,6 +26,20 @@ en: resend_confirmation: Resend confirmation instructions reset_password: Reset password set_new_password: Set new password + datetime: + distance_in_words: + about_x_hours: "%{count}h" + about_x_months: "%{count}mo" + about_x_years: "%{count}y" + almost_x_years: "%{count}y" + half_a_minute: Just now + less_than_x_minutes: "%{count}m" + less_than_x_seconds: Just now + over_x_years: "%{count}y" + x_days: "%{count}d" + x_minutes: "%{count}m" + x_months: "%{count}mo" + x_seconds: "%{count}s" generic: changes_saved_msg: Changes successfully saved! powered_by: powered by %{link} @@ -53,8 +67,13 @@ en: edit_profile: Edit profile preferences: Preferences stream_entries: + click_to_show: Click to show favourited: favourited a post by is_now_following: is now following + sensitive_content: Sensitive content + time: + formats: + default: "%b %d, %Y, %H:%M" users: invalid_email: The e-mail address is invalid will_paginate: